实现JWT身份认证机制,新增JWT认证过滤器和服务,更新链接生成接口以支持JWT验证,删除旧的用户控制器,添加JWT认证文档,增强错误处理和日志记录。
This commit is contained in:
160
docs/JWT认证使用说明.md
Normal file
160
docs/JWT认证使用说明.md
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
# JWT认证使用说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
系统已从手动传递操作者信息的头部参数改为使用JWT(JSON Web Token)进行身份认证。这种方式更加安全、标准化,并且符合REST API的最佳实践。
|
||||||
|
|
||||||
|
## 认证流程
|
||||||
|
|
||||||
|
### 1. 用户登录获取JWT令牌
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/auth/login
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"username": "your_username",
|
||||||
|
"password": "your_password"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
响应示例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"token": "eyJhbGciOiJIUzI1NiJ9...",
|
||||||
|
"userType": "AGENT",
|
||||||
|
"userId": 123,
|
||||||
|
"username": "your_username"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用JWT令牌调用受保护的接口
|
||||||
|
|
||||||
|
在请求头中添加 `Authorization` 头:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/link/generate
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"times": 10,
|
||||||
|
"perTimeQuantity": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 链接生成接口
|
||||||
|
|
||||||
|
### 接口地址
|
||||||
|
`POST /api/link/generate`
|
||||||
|
|
||||||
|
### 请求头
|
||||||
|
- `Authorization: Bearer {JWT_TOKEN}` - 必需,JWT认证令牌
|
||||||
|
- `Content-Type: application/json` - 必需
|
||||||
|
|
||||||
|
### 请求参数
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"times": 10, // 本次打脚本的次数
|
||||||
|
"perTimeQuantity": 5 // 每次打的数量
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 响应示例
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"batchId": 456,
|
||||||
|
"deductPoints": 50,
|
||||||
|
"expireAt": "2024-01-15T16:30:00",
|
||||||
|
"codeNos": ["ABC12345", "DEF67890", ...]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 权限控制
|
||||||
|
|
||||||
|
### 代理用户
|
||||||
|
- 只能为自己生成链接
|
||||||
|
- 必须检查积分余额
|
||||||
|
- 生成链接时扣除相应积分
|
||||||
|
|
||||||
|
### 管理员用户
|
||||||
|
- 只能为自己生成链接
|
||||||
|
- 跳过积分检查
|
||||||
|
- 不扣除积分
|
||||||
|
|
||||||
|
## 安全特性
|
||||||
|
|
||||||
|
1. **JWT令牌验证**:每个请求都会验证JWT令牌的有效性
|
||||||
|
2. **自动过期**:JWT令牌有自动过期时间(默认30分钟)
|
||||||
|
3. **用户身份验证**:从JWT中自动提取用户ID、用户类型等信息
|
||||||
|
4. **权限控制**:基于用户类型进行不同的业务逻辑处理
|
||||||
|
|
||||||
|
## 错误处理
|
||||||
|
|
||||||
|
### 常见错误
|
||||||
|
|
||||||
|
1. **缺少认证令牌**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "用户未认证"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **令牌无效或过期**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "JWT token validation failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **权限不足**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "非法操作者类型"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 核心组件
|
||||||
|
|
||||||
|
1. **JwtService**:JWT令牌的生成和解析
|
||||||
|
2. **JwtAuthenticationFilter**:自动处理JWT认证的Web过滤器
|
||||||
|
3. **SecurityConfig**:Spring Security配置,定义哪些接口需要认证
|
||||||
|
4. **LinkController**:使用Spring Security上下文获取用户信息
|
||||||
|
|
||||||
|
### 认证流程
|
||||||
|
|
||||||
|
```
|
||||||
|
请求 → JwtAuthenticationFilter → JWT解析 → 创建认证对象 → Spring Security上下文 → 控制器获取用户信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移说明
|
||||||
|
|
||||||
|
### 从旧版本迁移
|
||||||
|
|
||||||
|
如果您之前使用的是手动传递操作者信息的头部参数:
|
||||||
|
|
||||||
|
**旧方式(已废弃):**
|
||||||
|
```http
|
||||||
|
X-Operator-Id: 123
|
||||||
|
X-Operator-Type: AGENT
|
||||||
|
```
|
||||||
|
|
||||||
|
**新方式:**
|
||||||
|
```http
|
||||||
|
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 兼容性
|
||||||
|
|
||||||
|
- 旧的头参数方式已被完全移除
|
||||||
|
- 所有链接相关接口现在都需要JWT认证
|
||||||
|
- 确保在调用接口前先获取有效的JWT令牌
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
1. **令牌管理**:客户端应妥善保存JWT令牌,并在过期前刷新
|
||||||
|
2. **安全传输**:始终使用HTTPS传输JWT令牌
|
||||||
|
3. **错误处理**:客户端应处理认证失败的情况,引导用户重新登录
|
||||||
|
4. **日志记录**:系统会自动记录所有认证相关的操作日志
|
||||||
@@ -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.LinkGenerateRequest;
|
||||||
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
|
import com.gameplatform.server.model.dto.link.LinkGenerateResponse;
|
||||||
import com.gameplatform.server.service.link.LinkGenerationService;
|
import com.gameplatform.server.service.link.LinkGenerationService;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
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.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.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/link")
|
@RequestMapping("/api/link")
|
||||||
@Tag(name = "链接管理", description = "生成链接与二维码代理")
|
@Tag(name = "链接管理", description = "链接生成和管理相关接口")
|
||||||
public class LinkController {
|
public class LinkController {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LinkController.class);
|
||||||
|
|
||||||
private final LinkGenerationService linkGenerationService;
|
private final LinkGenerationService linkGenerationService;
|
||||||
|
|
||||||
public LinkController(LinkGenerationService linkGenerationService) {
|
public LinkController(LinkGenerationService linkGenerationService) {
|
||||||
@@ -22,27 +30,87 @@ public class LinkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/generate")
|
@PostMapping("/generate")
|
||||||
@ResponseStatus(HttpStatus.CREATED)
|
@ResponseStatus(HttpStatus.OK)
|
||||||
@Operation(summary = "生成链接批次", description = "times * perTimeQuantity = 目标点数;代理需扣点,管理员不扣")
|
@Operation(summary = "生成链接批次", description = "生成指定数量的链接批次。所有用户(管理员和代理商)都只能为自己生成链接,代理用户生成链接时会扣除积分,管理员生成链接时不扣除积分")
|
||||||
public Mono<LinkGenerateResponse> generate(@Valid @RequestBody LinkGenerateRequest req,
|
public Mono<LinkGenerateResponse> generateLinks(@Valid @RequestBody LinkGenerateRequest request,
|
||||||
@RequestHeader(value = "X-Operator-Id", required = false) Long operatorId,
|
Authentication authentication) {
|
||||||
@RequestHeader(value = "X-Operator-Type", required = false) String operatorType) {
|
log.info("=== 开始处理链接生成请求 ===");
|
||||||
// 暂时用两个 Header 传操作者信息;后续接入 JWT 解析
|
log.info("请求参数: times={}, perTimeQuantity={}",
|
||||||
Long targetAccountId = req.getAgentAccountId() != null ? req.getAgentAccountId() : operatorId;
|
request.getTimes(), request.getPerTimeQuantity());
|
||||||
return linkGenerationService.generateLinks(operatorId, safeUpper(operatorType), targetAccountId,
|
|
||||||
defaultInt(req.getTimes(), 0), defaultInt(req.getPerTimeQuantity(), 0))
|
log.info("=== 开始检查认证信息 ===");
|
||||||
.map(r -> {
|
log.info("Authentication: {}", authentication);
|
||||||
LinkGenerateResponse resp = new LinkGenerateResponse();
|
|
||||||
resp.setBatchId(r.getBatchId());
|
if (authentication == null) {
|
||||||
resp.setDeductPoints(r.getNeedPoints());
|
log.error("=== 认证失败:Authentication为空 ===");
|
||||||
resp.setExpireAt(r.getExpireAt());
|
return Mono.error(new IllegalArgumentException("用户未认证:Authentication为空"));
|
||||||
resp.setCodeNos(r.getCodeNos());
|
}
|
||||||
return resp;
|
|
||||||
|
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.bind.support.WebExchangeBindException;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
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.ConstraintViolation;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
@@ -22,21 +25,45 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(IllegalArgumentException.class)
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Object handleBadRequest(IllegalArgumentException e) {
|
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());
|
return body(HttpStatus.BAD_REQUEST.value(), e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(IllegalStateException.class)
|
@ExceptionHandler(IllegalStateException.class)
|
||||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||||
public Object handleForbidden(IllegalStateException e) {
|
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());
|
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)
|
@ExceptionHandler(WebExchangeBindException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Object handleBindException(WebExchangeBindException e) {
|
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>();
|
var details = new java.util.LinkedHashMap<String, Object>();
|
||||||
e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage()));
|
e.getFieldErrors().forEach(fe -> details.put(fe.getField(), fe.getDefaultMessage()));
|
||||||
e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage()));
|
e.getGlobalErrors().forEach(ge -> details.put(ge.getObjectName(), ge.getDefaultMessage()));
|
||||||
@@ -46,7 +73,8 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(ConstraintViolationException.class)
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Object handleConstraintViolation(ConstraintViolationException e) {
|
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>();
|
var details = new java.util.LinkedHashMap<String, Object>();
|
||||||
for (ConstraintViolation<?> v : e.getConstraintViolations()) {
|
for (ConstraintViolation<?> v : e.getConstraintViolations()) {
|
||||||
details.put(String.valueOf(v.getPropertyPath()), v.getMessage());
|
details.put(String.valueOf(v.getPropertyPath()), v.getMessage());
|
||||||
@@ -57,21 +85,24 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(ServerWebInputException.class)
|
@ExceptionHandler(ServerWebInputException.class)
|
||||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
public Object handleInput(ServerWebInputException e) {
|
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());
|
return body(HttpStatus.BAD_REQUEST.value(), "请求解析失败: " + e.getReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(ResponseStatusException.class)
|
@ExceptionHandler(ResponseStatusException.class)
|
||||||
public Object handleRse(ResponseStatusException e) {
|
public Object handleRse(ResponseStatusException e) {
|
||||||
var status = e.getStatusCode();
|
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());
|
return body(status.value(), e.getReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
public Object handleOther(Exception e) {
|
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(), "服务器内部错误");
|
return body(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,4 +122,12 @@ public class GlobalExceptionHandler {
|
|||||||
put("timestamp", Instant.now().toString());
|
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;
|
private Integer times;
|
||||||
@Schema(description = "每次打的数量", example = "5")
|
@Schema(description = "每次打的数量", example = "5")
|
||||||
private Integer perTimeQuantity;
|
private Integer perTimeQuantity;
|
||||||
@Schema(description = "为哪个代理账户生成(管理员可指定,代理可省略或为自己)")
|
|
||||||
private Long agentAccountId;
|
|
||||||
|
|
||||||
public Integer getTimes() { return times; }
|
public Integer getTimes() { return times; }
|
||||||
public void setTimes(Integer times) { this.times = times; }
|
public void setTimes(Integer times) { this.times = times; }
|
||||||
public Integer getPerTimeQuantity() { return perTimeQuantity; }
|
public Integer getPerTimeQuantity() { return perTimeQuantity; }
|
||||||
public void setPerTimeQuantity(Integer perTimeQuantity) { this.perTimeQuantity = 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();
|
byte[] bytes = secret.length() < 32 ? (secret + "_pad_to_32_chars_secret_key_value").getBytes() : secret.getBytes();
|
||||||
this.key = Keys.hmacShaKeyFor(bytes);
|
this.key = Keys.hmacShaKeyFor(bytes);
|
||||||
this.accessTokenMinutes = accessTokenMinutes;
|
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) {
|
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();
|
Instant now = Instant.now();
|
||||||
var builder = Jwts.builder()
|
var builder = Jwts.builder()
|
||||||
.setSubject(subject)
|
.setSubject(subject)
|
||||||
@@ -41,14 +46,44 @@ public class JwtService {
|
|||||||
if (extra != null) {
|
if (extra != null) {
|
||||||
extra.forEach(builder::claim);
|
extra.forEach(builder::claim);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("JWT builder配置完成,开始签名");
|
||||||
String token = builder.signWith(key, SignatureAlgorithm.HS256).compact();
|
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;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
public io.jsonwebtoken.Claims parse(String 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;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
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.annotation.web.reactive.EnableWebFluxSecurity;
|
||||||
|
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
|
||||||
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
import org.springframework.security.config.web.server.ServerHttpSecurity;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.server.SecurityWebFilterChain;
|
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
|
@Configuration
|
||||||
@EnableWebFluxSecurity
|
@EnableWebFluxSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
|
||||||
return http
|
log.info("=== 开始配置Spring Security安全链 ===");
|
||||||
|
|
||||||
|
SecurityWebFilterChain chain = http
|
||||||
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
.csrf(ServerHttpSecurity.CsrfSpec::disable)
|
||||||
.cors(cors -> {})
|
.cors(cors -> {})
|
||||||
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
|
||||||
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
|
||||||
|
.securityContextRepository(securityContextRepository())
|
||||||
|
.authenticationManager(authenticationManager())
|
||||||
.authorizeExchange(ex -> ex
|
.authorizeExchange(ex -> ex
|
||||||
.pathMatchers("/actuator/**").permitAll()
|
.pathMatchers("/actuator/**").permitAll()
|
||||||
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
.pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
|
||||||
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
.pathMatchers(HttpMethod.GET, "/api/auth/me").permitAll()
|
||||||
|
.pathMatchers("/api/link/**").authenticated() // 链接接口需要认证
|
||||||
.anyExchange().permitAll() // 其他接口后续再收紧
|
.anyExchange().permitAll() // 其他接口后续再收紧
|
||||||
)
|
)
|
||||||
|
// 关键:将JWT过滤器集成到Security过滤链中,放在AUTHENTICATION位置
|
||||||
|
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
.build();
|
.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
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
log.info("创建BCrypt密码编码器");
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,46 +46,43 @@ public class LinkGenerationService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType,
|
public Mono<GenerateResult> generateLinks(Long operatorId, String operatorType,
|
||||||
Long targetAccountId,
|
|
||||||
int times, int perTimeQuantity) {
|
int times, int perTimeQuantity) {
|
||||||
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, targetAccountId, times, perTimeQuantity))
|
return Mono.fromCallable(() -> doGenerate(operatorId, operatorType, times, perTimeQuantity))
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenerateResult doGenerate(Long operatorId, String operatorType,
|
private GenerateResult doGenerate(Long operatorId, String operatorType,
|
||||||
Long targetAccountId,
|
|
||||||
int times, int perTimeQuantity) {
|
int times, int perTimeQuantity) {
|
||||||
if (times <= 0 || perTimeQuantity <= 0) {
|
if (times <= 0 || perTimeQuantity <= 0) {
|
||||||
throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数");
|
throw new IllegalArgumentException("times 与 perTimeQuantity 必须为正整数");
|
||||||
}
|
}
|
||||||
|
|
||||||
UserAccount target = userAccountMapper.findById(targetAccountId);
|
// 获取操作者账户信息
|
||||||
if (target == null) {
|
UserAccount operator = userAccountMapper.findById(operatorId);
|
||||||
throw new IllegalArgumentException("目标账户不存在");
|
if (operator == null) {
|
||||||
|
throw new IllegalArgumentException("操作者账户不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType);
|
boolean isAdminOperator = "ADMIN".equalsIgnoreCase(operatorType);
|
||||||
if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) {
|
if (!isAdminOperator && !"AGENT".equalsIgnoreCase(operatorType)) {
|
||||||
throw new IllegalArgumentException("非法操作者类型");
|
throw new IllegalArgumentException("非法操作者类型");
|
||||||
}
|
}
|
||||||
if (!"AGENT".equalsIgnoreCase(target.getUserType())) {
|
|
||||||
throw new IllegalArgumentException("仅支持为代理账户生成链接");
|
|
||||||
}
|
|
||||||
|
|
||||||
long needPoints = (long) times * (long) perTimeQuantity;
|
long needPoints = (long) times * (long) perTimeQuantity;
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("generateLinks operatorId={} operatorType={} targetAccountId={} times={} perTimeQuantity={} needPoints={} expireHours={}",
|
log.debug("generateLinks operatorId={} operatorType={} times={} perTimeQuantity={} needPoints={} expireHours={}",
|
||||||
operatorId, operatorType, targetAccountId, times, perTimeQuantity, needPoints, expireHours);
|
operatorId, operatorType, times, perTimeQuantity, needPoints, expireHours);
|
||||||
}
|
}
|
||||||
if (!isAdminOperator) {
|
if (!isAdminOperator) {
|
||||||
// 代理商自操作,需扣点判断
|
// 代理商自操作,需扣点判断
|
||||||
long balance = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
|
long balance = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
|
||||||
if (balance < needPoints) {
|
if (balance < needPoints) {
|
||||||
throw new IllegalStateException("点数不足");
|
throw new IllegalStateException("点数不足");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LinkBatch batch = new LinkBatch();
|
LinkBatch batch = new LinkBatch();
|
||||||
batch.setAgentId(target.getId());
|
batch.setAgentId(operator.getId());
|
||||||
batch.setQuantity(times);
|
batch.setQuantity(times);
|
||||||
batch.setTimes(times);
|
batch.setTimes(times);
|
||||||
batch.setBatchSize(perTimeQuantity);
|
batch.setBatchSize(perTimeQuantity);
|
||||||
@@ -98,7 +95,7 @@ public class LinkGenerationService {
|
|||||||
for (int i = 0; i < times; i++) {
|
for (int i = 0; i < times; i++) {
|
||||||
LinkTask t = new LinkTask();
|
LinkTask t = new LinkTask();
|
||||||
t.setBatchId(batch.getId());
|
t.setBatchId(batch.getId());
|
||||||
t.setAgentId(target.getId());
|
t.setAgentId(operator.getId());
|
||||||
t.setCodeNo(generateCodeNo());
|
t.setCodeNo(generateCodeNo());
|
||||||
t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
|
t.setTokenHash(DigestUtils.sha256Hex(generateToken()));
|
||||||
t.setExpireAt(expireAt);
|
t.setExpireAt(expireAt);
|
||||||
@@ -109,11 +106,11 @@ public class LinkGenerationService {
|
|||||||
|
|
||||||
if (!isAdminOperator) {
|
if (!isAdminOperator) {
|
||||||
// 扣点流水 + 账户余额
|
// 扣点流水 + 账户余额
|
||||||
long before = target.getPointsBalance() == null ? 0L : target.getPointsBalance();
|
long before = operator.getPointsBalance() == null ? 0L : operator.getPointsBalance();
|
||||||
long after = before - needPoints;
|
long after = before - needPoints;
|
||||||
|
|
||||||
AgentPointsTx tx = new AgentPointsTx();
|
AgentPointsTx tx = new AgentPointsTx();
|
||||||
tx.setAccountId(target.getId());
|
tx.setAccountId(operator.getId());
|
||||||
tx.setType("DEDUCT");
|
tx.setType("DEDUCT");
|
||||||
tx.setBeforePoints(before);
|
tx.setBeforePoints(before);
|
||||||
tx.setDeltaPoints(-needPoints);
|
tx.setDeltaPoints(-needPoints);
|
||||||
@@ -124,7 +121,7 @@ public class LinkGenerationService {
|
|||||||
agentPointsTxMapper.insert(tx);
|
agentPointsTxMapper.insert(tx);
|
||||||
|
|
||||||
UserAccount patch = new UserAccount();
|
UserAccount patch = new UserAccount();
|
||||||
patch.setId(target.getId());
|
patch.setId(operator.getId());
|
||||||
patch.setPointsBalance(after);
|
patch.setPointsBalance(after);
|
||||||
userAccountMapper.update(patch);
|
userAccountMapper.update(patch);
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
BIN
target/classes/com/gameplatform/server/config/CorsConfig.class
Normal file
BIN
target/classes/com/gameplatform/server/config/CorsConfig.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/security/JwtService.class
Normal file
BIN
target/classes/com/gameplatform/server/security/JwtService.class
Normal file
Binary file not shown.
Binary file not shown.
BIN
target/classes/com/gameplatform/server/service/UserService.class
Normal file
BIN
target/classes/com/gameplatform/server/service/UserService.class
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,38 +8,34 @@ com\gameplatform\server\model\dto\common\PageResult.class
|
|||||||
com\gameplatform\server\service\UserService.class
|
com\gameplatform\server\service\UserService.class
|
||||||
com\gameplatform\server\controller\auth\AuthController.class
|
com\gameplatform\server\controller\auth\AuthController.class
|
||||||
com\gameplatform\server\GamePlatformServerApplication.class
|
com\gameplatform\server\GamePlatformServerApplication.class
|
||||||
|
com\gameplatform\server\service\link\LinkGenerationService$GenerateResult.class
|
||||||
com\gameplatform\server\mapper\admin\AnnouncementMapper.class
|
com\gameplatform\server\mapper\admin\AnnouncementMapper.class
|
||||||
com\gameplatform\server\model\dto\link\CreateLinkBatchResponse.class
|
|
||||||
com\gameplatform\server\service\auth\AuthService.class
|
com\gameplatform\server\service\auth\AuthService.class
|
||||||
com\gameplatform\server\config\SwaggerConfig.class
|
com\gameplatform\server\config\SwaggerConfig.class
|
||||||
com\gameplatform\server\mapper\account\UserAccountMapper.class
|
com\gameplatform\server\mapper\account\UserAccountMapper.class
|
||||||
com\gameplatform\server\service\account\AccountService.class
|
com\gameplatform\server\service\account\AccountService.class
|
||||||
com\gameplatform\server\model\dto\link\CreateLinkBatchRequest.class
|
com\gameplatform\server\security\JwtAuthenticationFilter.class
|
||||||
com\gameplatform\server\model\dto\link\LinkStatisticsResponse.class
|
com\gameplatform\server\service\external\ScriptClient.class
|
||||||
com\gameplatform\server\service\link\QrCodeProxyService$CachedImage.class
|
com\gameplatform\server\model\dto\link\LinkGenerateResponse.class
|
||||||
com\gameplatform\server\service\link\QrCodeProxyService.class
|
|
||||||
com\gameplatform\server\controller\auth\AuthController$1.class
|
com\gameplatform\server\controller\auth\AuthController$1.class
|
||||||
|
com\gameplatform\server\model\dto\link\LinkGenerateRequest.class
|
||||||
com\gameplatform\server\model\entity\admin\Announcement.class
|
com\gameplatform\server\model\entity\admin\Announcement.class
|
||||||
com\gameplatform\server\controller\link\LinkTaskController.class
|
|
||||||
com\gameplatform\server\model\dto\link\LinkTaskResponse.class
|
|
||||||
com\gameplatform\server\model\entity\agent\AgentPointsTx.class
|
com\gameplatform\server\model\entity\agent\AgentPointsTx.class
|
||||||
com\gameplatform\server\controller\admin\AccountController.class
|
com\gameplatform\server\controller\admin\AccountController.class
|
||||||
com\gameplatform\server\model\dto\auth\LoginRequest.class
|
com\gameplatform\server\model\dto\auth\LoginRequest.class
|
||||||
com\gameplatform\server\model\entity\account\UserAccount.class
|
com\gameplatform\server\model\entity\account\UserAccount.class
|
||||||
com\gameplatform\server\model\dto\auth\LoginResponse.class
|
com\gameplatform\server\model\dto\auth\LoginResponse.class
|
||||||
com\gameplatform\server\model\entity\agent\LinkTask.class
|
com\gameplatform\server\model\entity\agent\LinkTask.class
|
||||||
|
com\gameplatform\server\controller\link\LinkController.class
|
||||||
com\gameplatform\server\exception\GlobalExceptionHandler$1.class
|
com\gameplatform\server\exception\GlobalExceptionHandler$1.class
|
||||||
com\gameplatform\server\mapper\agent\LinkBatchMapper.class
|
com\gameplatform\server\mapper\agent\LinkBatchMapper.class
|
||||||
com\gameplatform\server\security\JwtService.class
|
com\gameplatform\server\security\JwtService.class
|
||||||
com\gameplatform\server\model\dto\link\LinkTaskQueryRequest.class
|
|
||||||
com\gameplatform\server\mapper\admin\OperationLogMapper.class
|
com\gameplatform\server\mapper\admin\OperationLogMapper.class
|
||||||
com\gameplatform\server\model\entity\agent\LinkBatch.class
|
com\gameplatform\server\model\entity\agent\LinkBatch.class
|
||||||
com\gameplatform\server\controller\UserController.class
|
|
||||||
com\gameplatform\server\security\SecurityConfig.class
|
com\gameplatform\server\security\SecurityConfig.class
|
||||||
com\gameplatform\server\service\link\LinkTaskService.class
|
|
||||||
com\gameplatform\server\model\entity\admin\OperationLog.class
|
com\gameplatform\server\model\entity\admin\OperationLog.class
|
||||||
com\gameplatform\server\model\dto\account\AccountResponse.class
|
com\gameplatform\server\model\dto\account\AccountResponse.class
|
||||||
com\gameplatform\server\model\dto\account\AccountCreateRequest.class
|
com\gameplatform\server\model\dto\account\AccountCreateRequest.class
|
||||||
com\gameplatform\server\config\ScheduleConfig.class
|
com\gameplatform\server\controller\link\QrProxyController.class
|
||||||
com\gameplatform\server\model\dto\account\AccountUpdateRequest.class
|
com\gameplatform\server\model\dto\account\AccountUpdateRequest.class
|
||||||
com\gameplatform\server\model\dto\account\ResetPasswordRequest.class
|
com\gameplatform\server\model\dto\account\ResetPasswordRequest.class
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\aut
|
|||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginRequest.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginRequest.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\admin\AccountController.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\admin\AccountController.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\external\ScriptClient.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\service\external\ScriptClient.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\UserController.java
|
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkBatchMapper.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkBatchMapper.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\QrProxyController.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\QrProxyController.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkTaskMapper.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\mapper\agent\LinkTaskMapper.java
|
||||||
@@ -25,6 +24,7 @@ D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\l
|
|||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\LinkTask.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\LinkTask.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\account\AccountUpdateRequest.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\account\AccountUpdateRequest.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\admin\Announcement.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\admin\Announcement.java
|
||||||
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\security\JwtAuthenticationFilter.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\AgentPointsTx.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\entity\agent\AgentPointsTx.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginResponse.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\model\dto\auth\LoginResponse.java
|
||||||
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\LinkController.java
|
D:\project\gamePlatform\server\src\main\java\com\gameplatform\server\controller\link\LinkController.java
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
com\gameplatform\server\service\link\LinkGenerationServiceTest.class
|
|
||||||
com\gameplatform\server\service\link\LinkTaskServiceTest.class
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
D:\project\gamePlatform\server\src\test\java\com\gameplatform\server\service\link\LinkTaskServiceTest.java
|
|
||||||
D:\project\gamePlatform\server\src\test\java\com\gameplatform\server\service\link\LinkGenerationServiceTest.java
|
|
||||||
Reference in New Issue
Block a user