This commit is contained in:
wangli 2026-01-22 22:54:08 +08:00
parent f27bd6055d
commit 17f557625b
27 changed files with 1726 additions and 433 deletions

View File

@ -0,0 +1,17 @@
package top.biwin.xinayu.common.dto.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 邮箱+邮箱验证码时的图形验证请求对象
*
* @author wangli
* @since 2026-01-22 12:54
*/
@Data
public class LoginCaptchaRequest {
@JsonProperty("session_id")
private String sessionId;
}

View File

@ -1,5 +1,6 @@
package top.biwin.xinayu.common.dto.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
@ -29,5 +30,6 @@ public class LoginRequest {
/**
* 邮箱验证码用于邮箱验证码登录
*/
@JsonProperty("verification_code")
private String verificationCode;
}

View File

@ -1,5 +1,6 @@
package top.biwin.xinayu.common.dto.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
@ -15,4 +16,12 @@ public class SendCodeRequest {
* 邮箱地址
*/
private String email;
/**
* 图形验证码ID
*/
@JsonProperty("session_id")
private String sessionId;
private String type;
}

View File

@ -0,0 +1,18 @@
package top.biwin.xinayu.common.dto.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 13:00
*/
@Data
public class VerifyLoginCaptchaRequest {
@JsonProperty("session_id")
private String sessionId;
@JsonProperty("captcha_code")
private String captchaCode;
}

View File

@ -0,0 +1,21 @@
package top.biwin.xinayu.common.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 13:01
*/
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class BaseResponse {
private String message;
private Boolean success;
}

View File

@ -0,0 +1,38 @@
package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 图形验证码响应 DTO
* 返回给客户端的验证码信息
*
* @author wangli
* @since 2026-01-22
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CaptchaResponse {
/**
* 验证码唯一标识UUID
*/
private String captchaId;
/**
* 验证码图片Base64编码
* 格式data:image/png;base64,iVBORw0KGgo...
*/
@JsonProperty("captcha_image")
private String captchaImage;
/**
* 验证码有效期
*/
private Integer expiresIn;
private Boolean success;
private String message;
}

View File

@ -0,0 +1,27 @@
package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 21:52
*/
@Data
public class CookieDetailsResponse {
private Long id;
private String cookie;
private Boolean enabled;
@JsonProperty("auto_confirm")
private Integer autoConfirm;
private String remark;
@JsonProperty("pause_duration")
private Integer pauseDuration;
private String username;
@JsonProperty("login_password")
private String loginPassword;
@JsonProperty("show_browser")
private Boolean showBrowser;
}

View File

@ -2,6 +2,7 @@ package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -13,55 +14,63 @@ import lombok.NoArgsConstructor;
* @since 2026-01-21
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginResponse {
/**
* 访问令牌短期有效用于访问受保护资源
*/
private String accessToken;
/**
* 访问令牌短期有效用于访问受保护资源
*/
private String accessToken;
/**
* 刷新令牌长期有效用于获取新的访问令牌
*/
private String refreshToken;
/**
* 刷新令牌长期有效用于获取新的访问令牌
*/
private String refreshToken;
/**
* 访问令牌过期时间
*/
private Long expiresIn;
/**
* 访问令牌过期时间
*/
private Long expiresIn;
/**
* 令牌类型固定为 "Bearer"
*/
private String tokenType = "Bearer";
/**
* 令牌类型固定为 "Bearer"
*/
private String tokenType = "Bearer";
@JsonProperty("is_admin")
private Boolean isAdmin;
@JsonProperty("is_admin")
private Boolean isAdmin;
@JsonProperty("user_id")
private Long userId;
@JsonProperty("user_id")
private Long userId;
private String username;
private String username;
/**
* 用户邮箱
*/
private String email;
/**
* 用户邮箱
*/
private String email;
/**
* 账号是否激活
*/
@JsonProperty("is_active")
private Boolean isActive;
/**
* 账号是否激活
*/
@JsonProperty("is_active")
private Boolean isActive;
/**
* 是否登陆成功
*/
private Boolean success;
/**
* 用户角色
* SUPER_ADMIN: 超级管理员
* ADMIN: 普通管理员
*/
private String role;
/**
* 错误信息
*/
private String message;
/**
* 是否登陆成功
*/
private Boolean success;
/**
* 错误信息
*/
private String message;
}

View File

@ -2,7 +2,9 @@ package top.biwin.xinayu.common.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 发送验证码响应 DTO
@ -11,22 +13,15 @@ import lombok.NoArgsConstructor;
* @author wangli
* @since 2026-01-21
*/
@EqualsAndHashCode(callSuper = true)
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class SendCodeResponse {
/**
* 是否发送成功
*/
private Boolean success;
public class SendCodeResponse extends BaseResponse {
/**
* 响应消息
*/
private String message;
/**
* 验证码过期时间
*/
private Integer expiresIn;
/**
* 验证码过期时间
*/
private Integer expiresIn;
}

View File

@ -0,0 +1,83 @@
package top.biwin.xinayu.common.enums;
/**
* 用户角色枚举
* 定义系统中的用户角色类型
*
* @author wangli
* @since 2026-01-22
*/
public enum UserRole {
/**
* 超级管理员
* 拥有系统所有权限可以管理所有用户和系统配置
*/
SUPER_ADMIN("超级管理员", "ROLE_SUPER_ADMIN"),
/**
* 普通管理员
* 拥有基本的管理权限但受到一定限制
*/
ADMIN("普通管理员", "ROLE_ADMIN");
/**
* 角色显示名称
*/
private final String displayName;
/**
* Spring Security 角色标识
* 遵循 Spring Security 规范 "ROLE_" 开头
*/
private final String authority;
UserRole(String displayName, String authority) {
this.displayName = displayName;
this.authority = authority;
}
/**
* 获取角色显示名称
*
* @return 显示名称
*/
public String getDisplayName() {
return displayName;
}
/**
* 获取 Spring Security 权限标识
*
* @return 权限标识 "ROLE_SUPER_ADMIN"
*/
public String getAuthority() {
return authority;
}
/**
* 检查是否为超级管理员
*
* @return true 如果是超级管理员
*/
public boolean isSuperAdmin() {
return this == SUPER_ADMIN;
}
/**
* 从字符串转换为角色枚举
*
* @param role 角色字符串
* @return UserRole 枚举值
*/
public static UserRole fromString(String role) {
if (role == null) {
return ADMIN; // 默认为普通管理员
}
try {
return UserRole.valueOf(role.toUpperCase());
} catch (IllegalArgumentException e) {
return ADMIN;
}
}
}

View File

@ -1,41 +1,49 @@
package top.biwin.xianyu.core.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import top.biwin.xinayu.common.enums.UserRole;
import java.time.LocalDateTime;
/**
* TODO
* 管理员用户实体
* 存储系统管理员的基本信息和角色权限
*
* @author wangli
* @since 2026-01-21 00:00
* @since 2026-01-21
*/
@Data
@Entity
@Table(name = "admin_user")
public class AdminUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, unique = true)
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(name = "password_hash", nullable = false)
@Column(name = "password_hash", nullable = false, length = 255)
private String passwordHash;
/**
* 用户角色
* SUPER_ADMIN: 超级管理员拥有所有权限
* ADMIN: 普通管理员基本权限
*/
@Enumerated(EnumType.STRING)
@Column(name = "role", nullable = false, length = 20)
@ColumnDefault("'ADMIN'")
private UserRole role = UserRole.ADMIN;
@Column(name = "is_active")
@ColumnDefault("true")
private Boolean isActive = true;
@ -47,4 +55,13 @@ public class AdminUser {
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* 检查当前用户是否为超级管理员
*
* @return true 如果是超级管理员
*/
public boolean isSuperAdmin() {
return role != null && role.isSuperAdmin();
}
}

View File

@ -22,50 +22,49 @@ import java.time.LocalDateTime;
@Entity
@Table(name = "goofish_account")
public class GoofishAccount {
@Id
private Long id;
@Id
private Long id;
@Column(nullable = false, length = 10000) // cookies can be long
private String cookie;
@Column(nullable = false, length = 10000) // cookies can be long
private String value;
@Column(name = "user_id", nullable = false)
@JsonProperty("user_id")
private Long userId;
@Column(name = "user_id", nullable = false)
@JsonProperty("user_id")
private Long userId;
@Column(name = "auto_confirm")
@ColumnDefault("1")
@JsonProperty("auto_confirm")
private Integer autoConfirm = 1;
@Column(name = "auto_confirm")
@ColumnDefault("1")
@JsonProperty("auto_confirm")
private Integer autoConfirm = 1;
@ColumnDefault("''")
private String remark;
@ColumnDefault("''")
private String remark;
@Column(name = "pause_duration")
@ColumnDefault("10")
@JsonProperty("pause_duration")
private Integer pauseDuration = 10;
@Column(name = "pause_duration")
@ColumnDefault("10")
@JsonProperty("pause_duration")
private Integer pauseDuration = 10;
@ColumnDefault("''")
private String username;
@ColumnDefault("''")
private String username;
@ColumnDefault("''")
private String password;
@ColumnDefault("''")
private String password;
@Column(name = "show_browser")
@ColumnDefault("0")
@JsonProperty("show_browser")
private Integer showBrowser = 0;
@Column(name = "show_browser")
@ColumnDefault("0")
@JsonProperty("show_browser")
private Integer showBrowser = 0;
@Column(name = "enabled")
@ColumnDefault("true")
private Boolean enabled = true;
@Column(name = "enabled")
@ColumnDefault("true")
private Boolean enabled = true;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,13 @@
package top.biwin.xianyu.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import top.biwin.xianyu.core.entity.GoofishAccount;
import java.util.Optional;
@Repository
public interface GoofishAccountRepository extends JpaRepository<GoofishAccount, Long> {
Optional<GoofishAccount> findByUserId(Long UserId);
}

View File

@ -6,6 +6,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@ -32,12 +33,14 @@ import top.biwin.xinayu.server.security.JwtAuthenticationFilter;
* - 禁用 CSRFREST API 场景
* - 白名单/auth/** 接口
* - 其他所有接口需要认证
* - 启用方法级安全支持 @PreAuthorize 等注解
*
* @author wangli
* @since 2026-01-21
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

View File

@ -4,18 +4,16 @@ import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;
import top.biwin.xinayu.common.dto.request.LoginRequest;
import top.biwin.xinayu.common.dto.request.RefreshRequest;
import top.biwin.xinayu.common.dto.request.SendCodeRequest;
import top.biwin.xinayu.common.dto.response.LoginResponse;
import top.biwin.xinayu.common.dto.response.RefreshResponse;
import top.biwin.xinayu.common.dto.response.SendCodeResponse;
import top.biwin.xinayu.server.security.JwtUtil;
import top.biwin.xinayu.server.service.AuthService;
import top.biwin.xinayu.server.service.EmailVerificationService;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.common.dto.request.*;
import top.biwin.xinayu.common.dto.response.*;
import top.biwin.xinayu.server.security.JwtUtil;
import top.biwin.xinayu.server.service.AuthService;
import top.biwin.xinayu.server.service.CaptchaService;
import top.biwin.xinayu.server.service.EmailVerificationService;
import java.util.HashMap;
import java.util.Map;
@ -24,11 +22,11 @@ import java.util.Optional;
/**
* 认证控制器
* 提供登录刷新令牌等认证相关的 REST API
*
* <p>
* 接口列表
* - POST /auth/login - 用户登录
* - POST /auth/refresh - 刷新访问令牌
*
* <p>
* 注意这些接口在 SecurityConfig 中已配置为白名单无需认证即可访问
*
* @author wangli
@ -38,200 +36,231 @@ import java.util.Optional;
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@Autowired
private AuthService authService;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private AdminUserRepository adminUserRepository;
/**
* 用户登录接口
*
* 请求示例
* POST /auth/login
* {
* "username": "admin",
* "password": "password123"
* }
*
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
*
* 错误响应
* - 401 Unauthorized: 用户名或密码错误
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 登录请求包含 username password
* @return ResponseEntity<LoginResponse> 登录响应
*/
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(response);
@Autowired
private CaptchaService captchaService;
/**
* 用户登录接口
* <p>
* 请求示例
* POST /auth/login
* {
* "username": "admin",
* "password": "password123"
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 用户名或密码错误
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 登录请求包含 username password
* @return ResponseEntity<LoginResponse> 登录响应
*/
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(response);
}
/**
* 刷新访问令牌接口
* <p>
* 请求示例
* POST /auth/refresh
* {
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 刷新令牌无效或已过期
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 刷新令牌请求包含 refreshToken
* @return ResponseEntity<RefreshResponse> 刷新令牌响应
*/
@PostMapping("/refresh")
public ResponseEntity<RefreshResponse> refresh(@RequestBody RefreshRequest request) {
RefreshResponse response = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(response);
}
/**
* 发送邮箱验证码接口
* <p>
*
* @param request 发送验证码请求包含 emailcaptchaIdcaptchaCode
* @return ResponseEntity<SendCodeResponse> 发送结果响应
*/
@PostMapping("/send-verification-code")
public ResponseEntity<SendCodeResponse> sendVerificationCode(@RequestBody SendCodeRequest request) {
Optional<AdminUser> optUser = adminUserRepository.findByEmail(request.getEmail());
SendCodeResponse response = SendCodeResponse.builder().build();
if (optUser.isEmpty()) {
response.setSuccess(false);
response.setMessage("该邮箱未注册账户");
} else {
// 验证通过发送邮箱验证码
emailVerificationService.generateAndSendCode(request.getEmail());
response.setSuccess(true);
response.setMessage("验证码已发送到您的邮箱,请注意查收");
response.setExpiresIn(300);
}
return ResponseEntity.ok(response);
}
/**
* 生成图形验证码接口
* <p>
* 生成4位字母数字混合验证码5分钟有效
* <p>
* 请求示例
* GET /auth/generate-captcha
* <p>
* 响应示例
* {
* "captchaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
* "captchaImage": "data:image/png;base64,iVBORw0KGgo...",
* "expiresIn": 300
* }
*
* @return ResponseEntity<CaptchaResponse> 验证码响应
*/
@PostMapping("/generate-captcha")
public ResponseEntity<CaptchaResponse> generateCaptcha(@RequestBody LoginCaptchaRequest request) {
CaptchaResponse response = captchaService.generateCaptcha(request.getSessionId());
return ResponseEntity.ok(response);
}
/**
* 验证图形验证码
* 对应 Python: /verify-captcha
*/
@PostMapping("/verify-captcha")
public ResponseEntity<BaseResponse> verifyCaptcha(@RequestBody VerifyLoginCaptchaRequest request) {
// 先验证图形验证码
if (!captchaService.verifyCaptcha(request.getSessionId(), request.getCaptchaCode())) {
throw new BadCredentialsException("图形验证码错误或已过期");
}
/**
* 刷新访问令牌接口
*
* 请求示例
* POST /auth/refresh
* {
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
*
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
*
* 错误响应
* - 401 Unauthorized: 刷新令牌无效或已过期
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 刷新令牌请求包含 refreshToken
* @return ResponseEntity<RefreshResponse> 刷新令牌响应
*/
@PostMapping("/refresh")
public ResponseEntity<RefreshResponse> refresh(@RequestBody RefreshRequest request) {
RefreshResponse response = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(response);
return ResponseEntity.ok(new BaseResponse("图形验证码验证成功", true));
}
/**
* 验证 Token 接口
* 检查 Access Token 是否有效并返回用户信息
* <p>
* 请求示例
* GET /auth/verify
* Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
* <p>
* 响应示例Token 有效
* {
* "authenticated": true,
* "user_id": 1,
* "username": "admin",
* "email": "admin@example.com",
* "is_admin": true
* }
* <p>
* 响应示例Token 无效
* {
* "authenticated": false
* }
*
* @param request HTTP 请求对象
* @return 验证结果和用户信息
*/
@GetMapping("/verify")
public ResponseEntity<Map<String, Object>> verify(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();
// 从请求头中提取 Token
String token = getTokenFromRequest(request);
if (token == null) {
response.put("authenticated", false);
response.put("message", "缺少 Authorization 头");
return ResponseEntity.ok(response);
}
/**
* 发送邮箱验证码接口
*
* 请求示例
* POST /auth/send-code
* {
* "email": "user@example.com"
* }
*
* 响应示例
* {
* "success": true,
* "message": "验证码已发送到您的邮箱",
* "expiresIn": 300
* }
*
* 错误响应
* - 400 Bad Request: 邮箱格式错误
* - 429 Too Many Requests: 发送过于频繁
* - 500 Internal Server Error: 邮件发送失败
*
* @param request 发送验证码请求包含 email
* @return ResponseEntity<SendCodeResponse> 发送结果响应
*/
@PostMapping("/send-code")
public ResponseEntity<SendCodeResponse> sendCode(@RequestBody SendCodeRequest request) {
emailVerificationService.generateAndSendCode(request.getEmail());
SendCodeResponse response = new SendCodeResponse(
true,
"验证码已发送到您的邮箱,请注意查收",
300
);
return ResponseEntity.ok(response);
}
try {
// 使用 JwtUtil 验证 Token 是否有效检查签名和过期时间
if (jwtUtil.validateToken(token)) {
// Token 有效提取用户名
String username = jwtUtil.getUsernameFromToken(token);
/**
* 验证 Token 接口
* 检查 Access Token 是否有效并返回用户信息
*
* 请求示例
* GET /auth/verify
* Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
*
* 响应示例Token 有效
* {
* "authenticated": true,
* "user_id": 1,
* "username": "admin",
* "email": "admin@example.com",
* "is_admin": true
* }
*
* 响应示例Token 无效
* {
* "authenticated": false
* }
*
* @param request HTTP 请求对象
* @return 验证结果和用户信息
*/
@GetMapping("/verify")
public ResponseEntity<Map<String, Object>> verify(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();
// 从请求头中提取 Token
String token = getTokenFromRequest(request);
if (token == null) {
response.put("authenticated", false);
response.put("message", "缺少 Authorization 头");
return ResponseEntity.ok(response);
// 从数据库查询用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isPresent()) {
AdminUser user = userOpt.get();
// 返回用户信息
response.put("authenticated", true);
response.put("user_id", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("is_admin", true); // admin_user 表的用户都是管理员
response.put("is_active", user.getIsActive());
} else {
// Token 有效但用户不存在可能已被删除
response.put("authenticated", false);
response.put("message", "用户不存在");
}
try {
// 使用 JwtUtil 验证 Token 是否有效检查签名和过期时间
if (jwtUtil.validateToken(token)) {
// Token 有效提取用户名
String username = jwtUtil.getUsernameFromToken(token);
// 从数据库查询用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isPresent()) {
AdminUser user = userOpt.get();
// 返回用户信息
response.put("authenticated", true);
response.put("user_id", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("is_admin", true); // admin_user 表的用户都是管理员
response.put("is_active", user.getIsActive());
} else {
// Token 有效但用户不存在可能已被删除
response.put("authenticated", false);
response.put("message", "用户不存在");
}
} else {
// Token 无效或已过期
response.put("authenticated", false);
response.put("message", "Token 无效或已过期");
}
} catch (Exception e) {
// Token 解析失败
response.put("authenticated", false);
response.put("message", "Token 格式错误");
}
return ResponseEntity.ok(response);
} else {
// Token 无效或已过期
response.put("authenticated", false);
response.put("message", "Token 无效或已过期");
}
} catch (Exception e) {
// Token 解析失败
response.put("authenticated", false);
response.put("message", "Token 格式错误");
}
/**
* 从请求头中提取 Token
*
* @param request HTTP 请求对象
* @return Token 字符串如果不存在则返回 null
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
return ResponseEntity.ok(response);
}
/**
* 从请求头中提取 Token
*
* @param request HTTP 请求对象
* @return Token 字符串如果不存在则返回 null
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}

View File

@ -0,0 +1,58 @@
package top.biwin.xinayu.server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.GoofishAccount;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xinayu.common.dto.response.CookieDetailsResponse;
import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 22:34
*/
@RestController
@RequestMapping("/goofish")
public class GoofishController {
@Autowired
private GoofishAccountRepository goofishAccountRepository;
/**
* 获取所有Cookie的详细信息包括值和状态
* 对应Python的 get_cookies_details 接口
*/
@GetMapping("/details")
public ResponseEntity<List<CookieDetailsResponse>> getAllCookiesDetails() {
List<GoofishAccount> goofishAccounts = new ArrayList<>();
if (CurrentUserUtil.isSuperAdmin()) {
goofishAccounts.addAll(goofishAccountRepository.findAll());
} else {
goofishAccountRepository.findByUserId(CurrentUserUtil.getCurrentUserId())
.ifPresent(goofishAccounts::add);
}
// 构建详细信息响应
return ResponseEntity.ok(goofishAccounts.stream().map(account -> {
CookieDetailsResponse response = new CookieDetailsResponse();
response.setId(account.getId());
response.setCookie(account.getCookie());
response.setEnabled(account.getEnabled());
response.setAutoConfirm(account.getAutoConfirm());
response.setRemark(account.getRemark() != null ? account.getRemark() : "");
response.setPauseDuration(account.getPauseDuration() != null ? account.getPauseDuration() : 10);
response.setUsername(account.getUsername());
response.setLoginPassword(account.getPassword());
response.setShowBrowser(account.getShowBrowser() == 1);
return response;
}).collect(Collectors.toList()));
}
}

View File

@ -0,0 +1,146 @@
package top.biwin.xinayu.server.controller.demo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xinayu.common.enums.UserRole;
import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.util.HashMap;
import java.util.Map;
/**
* CurrentUserUtil 使用示例控制器
* 演示如何使用 CurrentUserUtil 的各种方法
*
* @author wangli
* @since 2026-01-22
*/
@RestController
@RequestMapping("/api/current-user")
public class CurrentUserDemoController {
/**
* 示例1获取完整的 AdminUser 对象
*/
@GetMapping("/full-info")
public ResponseEntity<Map<String, Object>> getFullUserInfo() {
AdminUser user = CurrentUserUtil.getCurrentUser();
if (user == null) {
return ResponseEntity.ok(Map.of("message", "未登录"));
}
Map<String, Object> response = new HashMap<>();
response.put("id", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("role", user.getRole().name());
response.put("roleDisplay", user.getRole().getDisplayName());
response.put("isActive", user.getIsActive());
response.put("isSuperAdmin", user.isSuperAdmin());
response.put("createdAt", user.getCreatedAt());
response.put("updatedAt", user.getUpdatedAt());
return ResponseEntity.ok(response);
}
/**
* 示例2使用便捷方法获取用户信息
*/
@GetMapping("/quick-info")
public ResponseEntity<Map<String, Object>> getQuickInfo() {
Map<String, Object> response = new HashMap<>();
// 使用便捷方法
response.put("userId", CurrentUserUtil.getCurrentUserId());
response.put("username", CurrentUserUtil.getCurrentUsername());
response.put("email", CurrentUserUtil.getCurrentUserEmail());
response.put("role", CurrentUserUtil.getCurrentUserRole());
response.put("isSuperAdmin", CurrentUserUtil.isSuperAdmin());
response.put("isAuthenticated", CurrentUserUtil.isAuthenticated());
return ResponseEntity.ok(response);
}
/**
* 示例3基于用户信息的业务逻辑
*/
@GetMapping("/personalized-greeting")
public ResponseEntity<Map<String, Object>> personalizedGreeting() {
AdminUser user = CurrentUserUtil.getCurrentUser();
if (user == null) {
return ResponseEntity.ok(Map.of("greeting", "欢迎访客!"));
}
String greeting;
UserRole role = user.getRole();
if (role == UserRole.SUPER_ADMIN) {
greeting = String.format("尊敬的超级管理员 %s欢迎回来您拥有系统最高权限。",
user.getUsername());
} else {
greeting = String.format("您好,%s欢迎登录后台管理系统。",
user.getUsername());
}
Map<String, Object> response = new HashMap<>();
response.put("greeting", greeting);
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("accountAge", java.time.Duration.between(
user.getCreatedAt(),
java.time.LocalDateTime.now()
).toDays() + "");
return ResponseEntity.ok(response);
}
/**
* 示例4对比所有获取用户信息的方法
*/
@GetMapping("/method-comparison")
public ResponseEntity<Map<String, Object>> methodComparison() {
Map<String, Object> response = new HashMap<>();
// 方法1获取完整对象
AdminUser user = CurrentUserUtil.getCurrentUser();
if (user != null) {
response.put("method1_fullObject", Map.of(
"description", "通过 getCurrentUser() 获取完整对象",
"result", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail(),
"role", user.getRole().name()
)
));
}
// 方法2使用便捷方法
response.put("method2_quickAccess", Map.of(
"description", "使用便捷方法快速访问",
"result", Map.of(
"userId", CurrentUserUtil.getCurrentUserId(),
"username", CurrentUserUtil.getCurrentUsername(),
"email", CurrentUserUtil.getCurrentUserEmail(),
"role", CurrentUserUtil.getCurrentUserRole()
)
));
// 方法3角色判断
response.put("method3_roleCheck", Map.of(
"description", "使用角色判断方法",
"result", Map.of(
"isSuperAdmin", CurrentUserUtil.isSuperAdmin(),
"hasAdminRole", CurrentUserUtil.hasRole("ROLE_ADMIN"),
"hasSuperAdminRole", CurrentUserUtil.hasRole("ROLE_SUPER_ADMIN")
)
));
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,135 @@
package top.biwin.xinayu.server.controller.demo;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xinayu.server.security.RequireSuperAdmin;
import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.util.HashMap;
import java.util.Map;
/**
* 角色权限演示控制器
* 展示如何使用角色权限控制接口访问
*
* @author wangli
* @since 2026-01-22
*/
@RestController
@RequestMapping("/api/demo")
public class RolePermissionDemoController {
/**
* 示例1所有管理员都可以访问
* 任何已登录的用户ADMIN SUPER_ADMIN都可以访问
*/
@GetMapping("/admin-only")
public ResponseEntity<Map<String, Object>> adminOnly() {
String username = CurrentUserUtil.getCurrentUsername();
boolean isSuperAdmin = CurrentUserUtil.isSuperAdmin();
Map<String, Object> response = new HashMap<>();
response.put("message", "欢迎,管理员!");
response.put("username", username);
response.put("isSuperAdmin", isSuperAdmin);
response.put("accessLevel", "ADMIN");
return ResponseEntity.ok(response);
}
/**
* 示例2只有超级管理员可以访问使用自定义注解
* 使用 @RequireSuperAdmin 注解限制只有超级管理员能访问
*/
@RequireSuperAdmin
@GetMapping("/super-admin-only")
public ResponseEntity<Map<String, Object>> superAdminOnly() {
String username = CurrentUserUtil.getCurrentUsername();
Map<String, Object> response = new HashMap<>();
response.put("message", "欢迎,超级管理员!您拥有最高权限。");
response.put("username", username);
response.put("accessLevel", "SUPER_ADMIN");
return ResponseEntity.ok(response);
}
/**
* 示例3只有超级管理员可以访问使用 @PreAuthorize 注解
* 直接使用 Spring Security @PreAuthorize 注解
*/
@PreAuthorize("hasRole('SUPER_ADMIN')")
@GetMapping("/super-admin-settings")
public ResponseEntity<Map<String, Object>> superAdminSettings() {
String username = CurrentUserUtil.getCurrentUsername();
Map<String, Object> response = new HashMap<>();
response.put("message", "系统高级设置");
response.put("username", username);
response.put("settings", Map.of(
"maxUsers", 1000,
"enableDebugMode", true,
"systemVersion", "1.0.0"
));
return ResponseEntity.ok(response);
}
/**
* 示例4检查当前用户角色
* 返回当前用户的详细角色信息
*/
@GetMapping("/check-role")
public ResponseEntity<Map<String, Object>> checkRole() {
String username = CurrentUserUtil.getCurrentUsername();
boolean isSuperAdmin = CurrentUserUtil.isSuperAdmin();
boolean hasAdminRole = CurrentUserUtil.hasRole("ROLE_ADMIN");
boolean hasSuperAdminRole = CurrentUserUtil.hasRole("ROLE_SUPER_ADMIN");
Map<String, Object> response = new HashMap<>();
response.put("username", username);
response.put("isSuperAdmin", isSuperAdmin);
response.put("hasAdminRole", hasAdminRole);
response.put("hasSuperAdminRole", hasSuperAdminRole);
response.put("hasAnyAdminRole", CurrentUserUtil.hasAnyRole("ROLE_ADMIN", "ROLE_SUPER_ADMIN"));
return ResponseEntity.ok(response);
}
/**
* 示例5根据角色返回不同内容
* 超级管理员可以看到敏感信息普通管理员只能看到基本信息
*/
@GetMapping("/dashboard")
public ResponseEntity<Map<String, Object>> dashboard() {
String username = CurrentUserUtil.getCurrentUsername();
boolean isSuperAdmin = CurrentUserUtil.isSuperAdmin();
Map<String, Object> response = new HashMap<>();
response.put("username", username);
response.put("role", isSuperAdmin ? "SUPER_ADMIN" : "ADMIN");
// 基本统计所有管理员可见
Map<String, Object> basicStats = new HashMap<>();
basicStats.put("totalUsers", 150);
basicStats.put("activeUsers", 120);
response.put("basicStats", basicStats);
// 敏感数据只有超级管理员可见
if (isSuperAdmin) {
Map<String, Object> sensitiveData = new HashMap<>();
sensitiveData.put("totalRevenue", 1250000);
sensitiveData.put("systemErrors", 5);
sensitiveData.put("securityAlerts", 2);
response.put("sensitiveData", sensitiveData);
response.put("message", "您拥有完整的系统访问权限");
} else {
response.put("message", "基础仪表板数据");
}
return ResponseEntity.ok(response);
}
}

View File

@ -0,0 +1,167 @@
package top.biwin.xinayu.server.controller.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* 用户信息控制器示例
* 演示如何在 Controller 中获取当前登录用户信息
*
* @author wangli
* @since 2026-01-22
*/
@RestController
@RequestMapping("/api/user")
public class UserInfoController {
@Autowired
private AdminUserRepository adminUserRepository;
/**
* 方式1使用 CurrentUserUtil 工具类推荐
* <p>
* 优点简单直接代码简洁
* 适用场景Service Controller 层通用
*/
@GetMapping("/current/method1")
public ResponseEntity<Map<String, Object>> getCurrentUserMethod1() {
// 获取当前登录用户名
String username = CurrentUserUtil.getCurrentUsername();
if (username == null) {
return ResponseEntity.ok(Map.of("message", "未登录"));
}
// 根据用户名查询完整用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isEmpty()) {
return ResponseEntity.ok(Map.of("message", "用户不存在"));
}
AdminUser user = userOpt.get();
Map<String, Object> response = new HashMap<>();
response.put("userId", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("isActive", user.getIsActive());
response.put("method", "CurrentUserUtil");
return ResponseEntity.ok(response);
}
/**
* 方式2使用 @AuthenticationPrincipal 注解推荐
* <p>
* 优点Spring 自动注入类型安全
* 适用场景Controller
*/
@GetMapping("/current/method2")
public ResponseEntity<Map<String, Object>> getCurrentUserMethod2(
@AuthenticationPrincipal UserDetails userDetails) {
if (userDetails == null) {
return ResponseEntity.ok(Map.of("message", "未登录"));
}
String username = userDetails.getUsername();
// 根据用户名查询完整用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isEmpty()) {
return ResponseEntity.ok(Map.of("message", "用户不存在"));
}
AdminUser user = userOpt.get();
Map<String, Object> response = new HashMap<>();
response.put("userId", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("isActive", user.getIsActive());
response.put("method", "@AuthenticationPrincipal");
return ResponseEntity.ok(response);
}
/**
* 方式3使用 Authentication 参数
* <p>
* 优点可以获取更多认证信息如权限列表
* 适用场景需要权限信息的场景
*/
@GetMapping("/current/method3")
public ResponseEntity<Map<String, Object>> getCurrentUserMethod3(Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.ok(Map.of("message", "未登录"));
}
String username = authentication.getName();
// 根据用户名查询完整用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isEmpty()) {
return ResponseEntity.ok(Map.of("message", "用户不存在"));
}
AdminUser user = userOpt.get();
Map<String, Object> response = new HashMap<>();
response.put("userId", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("isActive", user.getIsActive());
response.put("authorities", authentication.getAuthorities());
response.put("method", "Authentication");
return ResponseEntity.ok(response);
}
/**
* 方式4使用 Principal 参数
* <p>
* 优点轻量级只包含用户名
* 适用场景仅需要用户名的场景
*/
@GetMapping("/current/method4")
public ResponseEntity<Map<String, Object>> getCurrentUserMethod4(Principal principal) {
if (principal == null) {
return ResponseEntity.ok(Map.of("message", "未登录"));
}
String username = principal.getName();
// 根据用户名查询完整用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isEmpty()) {
return ResponseEntity.ok(Map.of("message", "用户不存在"));
}
AdminUser user = userOpt.get();
Map<String, Object> response = new HashMap<>();
response.put("userId", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("isActive", user.getIsActive());
response.put("method", "Principal");
return ResponseEntity.ok(response);
}
}

View File

@ -6,6 +6,7 @@ import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import top.biwin.xinayu.common.dto.response.BaseResponse;
import java.time.LocalDateTime;
import java.util.HashMap;
@ -36,74 +37,6 @@ import java.util.Map;
@RestControllerAdvice
public class AuthenticationExceptionHandler {
/**
* 处理认证失败异常
* <p>
* 触发场景
* - 用户名或密码错误
* - JWT 令牌无效或过期
*
* @param ex 异常对象
* @return ResponseEntity<Map<String, Object>> 错误响应
*/
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<Map<String, Object>> handleBadCredentialsException(BadCredentialsException ex) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", HttpStatus.UNAUTHORIZED.value());
errorResponse.put("error", "Unauthorized");
errorResponse.put("message", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(errorResponse);
}
/**
* 处理用户不存在异常
* <p>
* 触发场景
* - 根据用户名查询用户时用户不存在
*
* @param ex 异常对象
* @return ResponseEntity<Map<String, Object>> 错误响应
*/
@ExceptionHandler(UsernameNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleUsernameNotFoundException(UsernameNotFoundException ex) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", HttpStatus.UNAUTHORIZED.value());
errorResponse.put("error", "Unauthorized");
errorResponse.put("message", ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(errorResponse);
}
/**
* 处理运行时异常
* <p>
* 触发场景
* - 用户被禁用
* - 业务逻辑异常
*
* @param ex 异常对象
* @return ResponseEntity<Map<String, Object>> 错误响应
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Map<String, Object>> handleRuntimeException(RuntimeException ex) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", HttpStatus.BAD_REQUEST.value());
errorResponse.put("error", "Bad Request");
errorResponse.put("message", ex.getMessage());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
/**
* 处理未知异常
* <p>
@ -111,18 +44,13 @@ public class AuthenticationExceptionHandler {
* - 其他未被捕获的异常
*
* @param ex 异常对象
* @return ResponseEntity<Map<String, Object>> 错误响应
* @return ResponseEntity<BaseResponse> 错误响应
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleException(Exception ex) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
errorResponse.put("error", "Internal Server Error");
errorResponse.put("message", "服务器内部错误: " + ex.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
public ResponseEntity<BaseResponse> handleException(Exception ex) {
return ResponseEntity.ok(BaseResponse.builder()
.success(false)
.message(ex.getMessage())
.build());
}
}

View File

@ -0,0 +1,28 @@
package top.biwin.xinayu.server.security;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.*;
/**
* 超级管理员权限注解
* 使用此注解标记的方法或类只能由超级管理员访问
* <p>
* 使用示例
* <pre>
* &#64;RequireSuperAdmin
* &#64;GetMapping("/admin/users")
* public List&lt;User&gt; getAllUsers() {
* // 只有超级管理员可以访问
* }
* </pre>
*
* @author wangli
* @since 2026-01-22
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@PreAuthorize("hasRole('SUPER_ADMIN')")
public @interface RequireSuperAdmin {
}

View File

@ -1,6 +1,7 @@
package top.biwin.xinayu.server.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
@ -8,19 +9,19 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.common.enums.UserRole;
import java.util.ArrayList;
import java.util.Collections;
/**
* AdminUser 用户详情服务
* 实现 Spring Security UserDetailsService 接口
* <p>
* 这是主人实现自定义用户查询逻辑的核心位置
* <p>
* 主要职责
* 1. 根据用户名从数据库查询 AdminUser
* 2. 检查用户是否存在和是否激活
* 3. AdminUser 转换为 Spring Security UserDetails
* 4. 为用户分配对应的角色权限
*
* @author wangli
* @since 2026-01-21
@ -28,34 +29,42 @@ import java.util.ArrayList;
@Service
public class AdminUserDetailsService implements UserDetailsService {
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private AdminUserRepository adminUserRepository;
/**
* 根据用户名加载用户信息
* <p>
* 主人的自定义逻辑实现位置 #1
* <p>
* 💡 扩展提示
* - 可以在这里添加登录失败次数限制需要在 AdminUser 实体中添加 failedLoginAttempts 字段
* - 可以添加 IP 白名单验证
* - 可以添加账号锁定逻辑
* - 如果后续需要角色权限可以在 AdminUser 中添加 roles 字段并在这里设置 authorities
*
* @param username 用户名
* @return UserDetails Spring Security 的用户详情对象
* @throws UsernameNotFoundException 用户不存在时抛出
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AdminUser adminUser = adminUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
if (!adminUser.getIsActive()) { throw new RuntimeException("用户已被禁用: " + username); }
return User.builder()
.username(adminUser.getUsername())
.password(adminUser.getPasswordHash())
.authorities(new ArrayList<>())
.build();
/**
* 根据用户名加载用户信息
* <p>
* 加载用户信息并设置角色权限
* - SUPER_ADMIN -> ROLE_SUPER_ADMIN
* - ADMIN -> ROLE_ADMIN
*
* @param username 用户名
* @return UserDetails Spring Security 的用户详情对象包含角色权限
* @throws UsernameNotFoundException 用户不存在时抛出
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 从数据库查询用户
AdminUser adminUser = adminUserRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
}
// 2. 检查用户是否被禁用
if (!adminUser.getIsActive()) {
throw new RuntimeException("用户已被禁用: " + username);
}
// 3. 获取用户角色如果为空则默认为 ADMIN
UserRole role = adminUser.getRole() != null ? adminUser.getRole() : UserRole.ADMIN;
// 4. 将角色转换为 Spring Security GrantedAuthority
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getAuthority());
// 5. 构建并返回 UserDetails 对象
return User.builder()
.username(adminUser.getUsername())
.password(adminUser.getPasswordHash())
.authorities(Collections.singletonList(authority))
.build();
}
}

View File

@ -1,6 +1,5 @@
package top.biwin.xinayu.server.service;
import cn.hutool.log.Log;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
@ -10,15 +9,13 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.common.dto.request.LoginRequest;
import top.biwin.xinayu.common.dto.response.LoginResponse;
import top.biwin.xinayu.common.dto.response.RefreshResponse;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.server.security.JwtUtil;
import java.util.Optional;
/**
* 认证服务
* 提供登录刷新令牌等认证相关功能
@ -69,30 +66,33 @@ public class AuthService {
// TODO: 主人这里判断使用哪种登录方式
// 根据 request 中的字段判断登录类型然后调用对应的认证方法
if (StringUtils.hasText(request.getUsername()) && StringUtils.hasText(request.getPassword())) {
// 方式1用户名 + 密码登录
user = authenticateByUsername(request.getUsername(), request.getPassword());
log.info("✅ 用户名登录成功: {}", request.getUsername());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getPassword())) {
// 方式2邮箱 + 密码登录
user = authenticateByEmail(request.getEmail(), request.getPassword());
log.info("✅ 邮箱密码登录成功: {}", request.getEmail());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getVerificationCode())) {
// 方式3邮箱 + 验证码登录
user = authenticateByEmailCode(request.getEmail(), request.getVerificationCode());
log.info("✅ 邮箱验证码登录成功: {}", request.getEmail());
} else {
throw new BadCredentialsException("无效的登录参数组合");
return LoginResponse.builder()
.success(false)
.message("无效的登录参数组合")
.build();
}
// TODO: 主人这里生成 JWT Token 并构建响应
// 1. 使用 user.getUsername() 生成 accessToken refreshToken
// 2. 调用 buildLoginResponse() 方法构建包含完整用户信息的响应
// 生成 JWT Token
String accessToken = jwtUtil.generateAccessToken(user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());
@ -115,7 +115,7 @@ public class AuthService {
// 1. 创建 UsernamePasswordAuthenticationToken
// 2. 调用 authenticationManager.authenticate(authToken)
// 3. 认证成功后从数据库查询用户信息并返回
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authToken);
@ -128,7 +128,7 @@ public class AuthService {
/**
* 邮箱 + 密码认证
*
* @param email 邮箱
* @param email 邮箱
* @param password 密码
* @return AdminUser 用户实体
*/
@ -141,7 +141,7 @@ public class AuthService {
// 4. 如果密码错误抛出 BadCredentialsException
// 5. 检查账号是否激活
// 6. 返回用户实体
AdminUser user = adminUserRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("邮箱或密码错误"));
@ -162,7 +162,7 @@ public class AuthService {
* 邮箱 + 验证码认证
*
* @param email 邮箱
* @param code 验证码
* @param code 验证码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByEmailCode(String email, String code) {
@ -173,7 +173,7 @@ public class AuthService {
// 3. 通过邮箱查询用户
// 4. 检查账号是否激活
// 5. 返回用户实体
// 验证验证码
if (!emailVerificationService.verifyCode(email, code)) {
throw new BadCredentialsException("验证码无效或已过期");
@ -194,31 +194,36 @@ public class AuthService {
/**
* 构建登录响应包含完整用户信息
*
* @param user 用户实体
* @param accessToken 访问令牌
* @param user 用户实体
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param expiresIn 过期时间
* @param expiresIn 过期时间
* @return LoginResponse
*/
private LoginResponse buildLoginResponse(AdminUser user, String accessToken,
private LoginResponse buildLoginResponse(AdminUser user, String accessToken,
String refreshToken, Long expiresIn) {
// TODO: 主人这里构建包含完整用户信息的响应
// user 实体的信息映射到 LoginResponse 的各个字段
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(expiresIn);
response.setTokenType("Bearer");
// 设置用户信息
response.setUserId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setIsActive(user.getIsActive());
response.setIsAdmin(true); // 所有 admin_user 表的用户都是管理员
response.setSuccess(true);
// 设置角色信息
String roleName = user.getRole() != null ? user.getRole().name() : "ADMIN";
response.setRole(roleName);
response.setSuccess(true);
return response;
}

View File

@ -0,0 +1,193 @@
package top.biwin.xinayu.server.service;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import top.biwin.xinayu.common.dto.response.CaptchaResponse;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* 图形验证码服务
* 负责生成存储和验证图形验证码
* <p>
* 使用 Hutool CaptchaUtil 生成验证码图片
* 使用 ConcurrentHashMap 在内存中存储验证码
*
* @author wangli
* @since 2026-01-22
*/
@Slf4j
@Service
public class CaptchaService {
/**
* 验证码存储内存
* Key: captchaId, Value: CaptchaData
*/
private final Map<String, CaptchaData> captchaStore = new ConcurrentHashMap<>();
/**
* 验证码有效期分钟
*/
private static final int CAPTCHA_EXPIRE_MINUTES = 5;
/**
* 验证码长度
*/
private static final int CAPTCHA_LENGTH = 4;
/**
* 生成图形验证码
* <p>
* 生成4位字母数字混合验证码包含干扰线
* 验证码5分钟后过期
*
* @return CaptchaResponse 包含验证码ID和Base64图片
*/
public CaptchaResponse generateCaptcha(String sessionId) {
// TODO: 主人这里使用 Hutool 生成验证码
// 步骤
// 1. 使用 CaptchaUtil.createLineCaptcha(, , 验证码长度, 干扰线数量)
// 2. 生成唯一的 captchaId使用 UUID
// 3. 获取验证码文本captcha.getCode()
// 4. 存储验证码到内存设置过期时间
// 5. 获取Base64图片captcha.getImageBase64Data()
// 6. 返回 CaptchaResponse
// 创建线性验证码宽120高404位验证码50条干扰线
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 40, CAPTCHA_LENGTH, 50);
// 获取验证码文本
String code = captcha.getCode();
// 计算过期时间
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(CAPTCHA_EXPIRE_MINUTES);
// 存储验证码
CaptchaData data = new CaptchaData(code, expiresAt);
captchaStore.put(sessionId, data);
// 获取Base64图片包含 data:image/png;base64, 前缀
String base64Image = captcha.getImageBase64Data();
log.info("✅ 生成图形验证码ID: {}, 过期时间: {}", sessionId, expiresAt);
return new CaptchaResponse(sessionId, base64Image, CAPTCHA_EXPIRE_MINUTES * 60, true, "图形验证码生成成功");
}
/**
* 验证图形验证码
* <p>
* 验证成功后立即删除验证码一次性使用
* 验证码不区分大小写
*
* @param captchaId 验证码ID
* @param userInput 用户输入的验证码
* @return 是否验证成功
*/
public boolean verifyCaptcha(String captchaId, String userInput) {
// TODO: 主人这里验证验证码是否正确
// 步骤
// 1. captchaStore 获取验证码数据
// 2. 检查是否存在
// 3. 检查是否过期
// 4. 比较验证码不区分大小写
// 5. 验证成功后立即删除一次性使用
if (captchaId == null || userInput == null) {
log.warn("⚠️ 验证码参数为空");
return false;
}
// 获取存储的验证码数据
CaptchaData data = captchaStore.get(captchaId);
if (data == null) {
log.warn("⚠️ 验证码不存在ID: {}", captchaId);
return false;
}
// 检查是否过期
if (data.getExpiresAt().isBefore(LocalDateTime.now())) {
captchaStore.remove(captchaId);
log.warn("⚠️ 验证码已过期ID: {}", captchaId);
return false;
}
// 验证码比较不区分大小写
boolean isValid = data.getCode().equalsIgnoreCase(userInput);
if (isValid) {
// 验证成功立即删除一次性使用
captchaStore.remove(captchaId);
log.info("✅ 验证码验证成功ID: {}", captchaId);
} else {
log.warn("⚠️ 验证码错误ID: {}, 期望: {}, 输入: {}",
captchaId, data.getCode(), userInput);
}
return isValid;
}
/**
* 定时清理过期验证码
* 每分钟执行一次防止内存泄漏
*/
@Scheduled(fixedRate = 60000)
public void cleanExpiredCaptcha() {
// TODO: 主人这里清理过期的验证码
// 遍历 captchaStore删除过期的验证码
LocalDateTime now = LocalDateTime.now();
int removedCount = 0;
// 使用 Iterator 安全删除
captchaStore.entrySet().removeIf(entry -> {
if (entry.getValue().getExpiresAt().isBefore(now)) {
return true;
}
return false;
});
if (removedCount > 0) {
log.info("🧹 清理过期验证码,数量: {}", removedCount);
}
}
/**
* 获取当前验证码数量用于监控
*
* @return 验证码数量
*/
public int getCaptchaCount() {
return captchaStore.size();
}
/**
* 验证码数据内部类
*/
@Data
private static class CaptchaData {
/**
* 验证码文本
*/
private final String code;
/**
* 过期时间
*/
private final LocalDateTime expiresAt;
public CaptchaData(String code, LocalDateTime expiresAt) {
this.code = code;
this.expiresAt = expiresAt;
}
}
}

View File

@ -105,14 +105,20 @@ public class EmailService {
// 动态创建 MailSender
JavaMailSender mailSender = createMailSender();
// 获取发件人显示名如果配置了
String smtpFrom = getSettingValue("smtp_from");
// 获取发件人邮箱必须是真实邮箱地址
String smtpUser = getSettingValue("smtp_user");
String fromAddress = StringUtils.hasText(smtpFrom) ? smtpFrom : smtpUser;
String smtpFrom = getSettingValue("smtp_from");
// 创建邮件消息
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromAddress);
// 设置发件人如果有显示名使用 "显示名 <邮箱>" 格式否则只用邮箱
if (StringUtils.hasText(smtpFrom)) {
message.setFrom(smtpFrom + " <" + smtpUser + ">");
} else {
message.setFrom(smtpUser);
}
message.setTo(toEmail);
message.setSubject("【闲鱼自由】邮箱验证码");
message.setText(
@ -146,14 +152,20 @@ public class EmailService {
// 动态创建 MailSender
JavaMailSender mailSender = createMailSender();
// 获取发件人显示名
String smtpFrom = getSettingValue("smtp_from");
// 获取发件人邮箱必须是真实邮箱地址
String smtpUser = getSettingValue("smtp_user");
String fromAddress = StringUtils.hasText(smtpFrom) ? smtpFrom : smtpUser;
String smtpFrom = getSettingValue("smtp_from");
// 创建邮件消息
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(fromAddress);
// 设置发件人如果有显示名使用 "显示名 <邮箱>" 格式否则只用邮箱
if (StringUtils.hasText(smtpFrom)) {
message.setFrom(smtpFrom + " <" + smtpUser + ">");
} else {
message.setFrom(smtpUser);
}
message.setTo(toEmail);
message.setSubject(subject);
message.setText(content);

View File

@ -0,0 +1,272 @@
package top.biwin.xinayu.server.util;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 当前用户工具类
* 提供简便的方法获取当前登录用户信息
* <p>
* 使用场景
* - Service 层获取当前用户
* - Controller 层获取当前用户
* - 记录操作日志时获取操作人
*
* @author wangli
* @since 2026-01-22
*/
public class CurrentUserUtil {
/**
* 获取当前登录用户的用户名
* <p>
* 使用示例
* <pre>
* String username = CurrentUserUtil.getCurrentUsername();
* </pre>
*
* @return 用户名如果未登录则返回 null
*/
public static String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
} else if (principal instanceof String) {
return (String) principal;
}
return null;
}
/**
* 获取当前登录用户的 UserDetails
* <p>
* 使用示例
* <pre>
* UserDetails userDetails = CurrentUserUtil.getCurrentUserDetails();
* if (userDetails != null) {
* String username = userDetails.getUsername();
* Collection&lt;? extends GrantedAuthority&gt; authorities = userDetails.getAuthorities();
* }
* </pre>
*
* @return UserDetails 对象如果未登录则返回 null
*/
public static UserDetails getCurrentUserDetails() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
return (UserDetails) principal;
}
return null;
}
/**
* 获取当前登录用户的 Authentication 对象
* <p>
* 使用示例
* <pre>
* Authentication auth = CurrentUserUtil.getCurrentAuthentication();
* if (auth != null) {
* String username = auth.getName();
* Collection&lt;? extends GrantedAuthority&gt; authorities = auth.getAuthorities();
* }
* </pre>
*
* @return Authentication 对象如果未登录则返回 null
*/
public static Authentication getCurrentAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 检查当前用户是否已登录
* <p>
* 使用示例
* <pre>
* if (CurrentUserUtil.isAuthenticated()) {
* // 用户已登录
* }
* </pre>
*
* @return true 如果用户已登录false 否则
*/
public static boolean isAuthenticated() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null
&& authentication.isAuthenticated()
&& !(authentication.getPrincipal() instanceof String && "anonymousUser".equals(authentication.getPrincipal()));
}
/**
* 检查当前用户是否为超级管理员
* <p>
* 使用示例
* <pre>
* if (CurrentUserUtil.isSuperAdmin()) {
* // 执行超管专属操作
* }
* </pre>
*
* @return true 如果当前用户是超级管理员false 否则
*/
public static boolean isSuperAdmin() {
Authentication authentication = getCurrentAuthentication();
if (authentication == null || authentication.getAuthorities() == null) {
return false;
}
return authentication.getAuthorities().stream()
.anyMatch(authority -> "ROLE_SUPER_ADMIN".equals(authority.getAuthority()));
}
/**
* 检查当前用户是否拥有指定角色
* <p>
* 使用示例
* <pre>
* if (CurrentUserUtil.hasRole("ROLE_ADMIN")) {
* // 用户拥有 ADMIN 角色
* }
* </pre>
*
* @param role 角色名称格式ROLE_XXX
* @return true 如果拥有指定角色false 否则
*/
public static boolean hasRole(String role) {
Authentication authentication = getCurrentAuthentication();
if (authentication == null || authentication.getAuthorities() == null) {
return false;
}
return authentication.getAuthorities().stream()
.anyMatch(authority -> role.equals(authority.getAuthority()));
}
/**
* 检查当前用户是否拥有任意一个指定角色
* <p>
* 使用示例
* <pre>
* if (CurrentUserUtil.hasAnyRole("ROLE_SUPER_ADMIN", "ROLE_ADMIN")) {
* // 用户拥有其中任意一个角色
* }
* </pre>
*
* @param roles 角色名称数组
* @return true 如果拥有任意一个角色false 否则
*/
public static boolean hasAnyRole(String... roles) {
Authentication authentication = getCurrentAuthentication();
if (authentication == null || authentication.getAuthorities() == null || roles == null) {
return false;
}
for (String role : roles) {
if (authentication.getAuthorities().stream()
.anyMatch(authority -> role.equals(authority.getAuthority()))) {
return true;
}
}
return false;
}
/**
* 获取当前登录用户的完整 AdminUser 对象
* <p>
* 使用示例
* <pre>
* AdminUser user = CurrentUserUtil.getCurrentUser();
* if (user != null) {
* Long userId = user.getId();
* String email = user.getEmail();
* UserRole role = user.getRole();
* }
* </pre>
*
* @return AdminUser 对象如果未登录或用户不存在则返回 null
*/
public static top.biwin.xianyu.core.entity.AdminUser getCurrentUser() {
// 1. 获取当前用户名
String username = getCurrentUsername();
if (username == null) {
return null;
}
// 2. 通过 SpringContextHolder 获取 AdminUserRepository
try {
top.biwin.xianyu.core.repository.AdminUserRepository repository =
SpringContextHolder.getBean(top.biwin.xianyu.core.repository.AdminUserRepository.class);
// 3. 查询并返回 AdminUser 对象
return repository.findByUsername(username).orElse(null);
} catch (Exception e) {
// 如果获取失败例如 ApplicationContext 未初始化返回 null
return null;
}
}
/**
* 获取当前登录用户的 ID
* <p>
* 使用示例
* <pre>
* Long userId = CurrentUserUtil.getCurrentUserId();
* </pre>
*
* @return 用户 ID如果未登录则返回 null
*/
public static Long getCurrentUserId() {
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
return user != null ? user.getId() : null;
}
/**
* 获取当前登录用户的邮箱
* <p>
* 使用示例
* <pre>
* String email = CurrentUserUtil.getCurrentUserEmail();
* </pre>
*
* @return 用户邮箱如果未登录则返回 null
*/
public static String getCurrentUserEmail() {
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
return user != null ? user.getEmail() : null;
}
/**
* 获取当前登录用户的角色
* <p>
* 使用示例
* <pre>
* UserRole role = CurrentUserUtil.getCurrentUserRole();
* if (role == UserRole.SUPER_ADMIN) {
* // 执行超管操作
* }
* </pre>
*
* @return 用户角色枚举如果未登录则返回 null
*/
public static top.biwin.xinayu.common.enums.UserRole getCurrentUserRole() {
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
return user != null ? user.getRole() : null;
}
}

View File

@ -0,0 +1,60 @@
package top.biwin.xinayu.server.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring Context 持有者
* 用于在非 Spring 管理的类中获取 Spring Bean
*
* @author wangli
* @since 2026-01-22
*/
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = context;
}
/**
* 获取 ApplicationContext
*
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 根据类型获取 Bean
*
* @param clazz Bean 类型
* @param <T> 泛型类型
* @return Bean 实例
*/
public static <T> T getBean(Class<T> clazz) {
if (applicationContext == null) {
throw new IllegalStateException("ApplicationContext 未初始化");
}
return applicationContext.getBean(clazz);
}
/**
* 根据名称获取 Bean
*
* @param name Bean 名称
* @return Bean 实例
*/
public static Object getBean(String name) {
if (applicationContext == null) {
throw new IllegalStateException("ApplicationContext 未初始化");
}
return applicationContext.getBean(name);
}
}