refactor(project): Reorganize frontend and backend directories

This commit is contained in:
2026-03-18 10:45:15 +08:00
parent cc9eaf4cdf
commit 92a139e470
513 changed files with 28251 additions and 287 deletions
+8
View File
@@ -0,0 +1,8 @@
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/video-auth-*.jar app.jar
# 添加JVM内存优化配置
ENTRYPOINT ["sh", "-c", "java ${JAVA_OPTS} -jar app.jar"]
+140
View File
@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>video-platform</artifactId>
<groupId>com.nuaa.video</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>video-auth</artifactId>
<dependencies>
<!-- 引入公共模块 -->
<dependency>
<groupId>com.nuaa.video</groupId>
<artifactId>video-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security OAuth2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- Spring Security OAuth2 Authorization Server -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.11</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,16 @@
package com.nuaa.video.auth;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
@@ -0,0 +1,38 @@
package com.nuaa.video.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 创建配置好的ObjectMapper
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
// 使用Jackson2JsonRedisSerializer
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
@@ -0,0 +1,85 @@
package com.nuaa.video.auth.config;
import cn.hutool.jwt.Claims;
import com.nuaa.video.auth.filter.JwtAuthenticationFilter;
import com.nuaa.video.auth.service.AuthUserService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.View;
import java.io.IOException;
/**
* 安全配置
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final AuthUserService authUserService;
// 使用构造注入并添加@Lazy注解
public SecurityConfig(@Lazy AuthUserService authUserService) {
this.authUserService = authUserService;
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证管理器
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* 安全过滤器链
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, View error, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/auth/login",
"/auth/register",
"/auth/captcha",
"/actuator/**",
"/error"
).permitAll()
.anyRequest().authenticated()
)
//添加JWT认证过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@@ -0,0 +1,49 @@
package com.nuaa.video.auth.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.service.AuthUserService;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/auth/admin")
@RequiredArgsConstructor
public class AdminAuthController {
private final AuthUserService authUserService;
/**
* 获取登录统计 - 管理接口
*/
@GetMapping("/login/stats")
public Result<Object> getLoginStats() {
return authUserService.getLoginStats();
}
/**
* 管理员重置用户密码 - 管理接口
*/
@PutMapping("/reset-password/{userId}")
public Result<Void> resetPassword(
@PathVariable Long userId,
@RequestParam String newPassword) {
return authUserService.adminResetPassword(userId, newPassword);
}
/**
* 锁定/解锁用户账号 - 管理接口
*/
@PutMapping("/lock/{userId}/{locked}")
public Result<Void> lockUser(
@PathVariable Long userId,
@PathVariable Boolean locked) {
return authUserService.lockUser(userId, locked);
}
}
@@ -0,0 +1,125 @@
package com.nuaa.video.auth.controller;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import com.nuaa.video.auth.dto.TokenInfoDTO;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.service.AuthUserService;
import com.nuaa.video.auth.service.CaptchaService;
import com.nuaa.video.common.model.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 认证控制器
*/
@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthUserService authUserService;
private final CaptchaService captchaService;
/**
* 用户登录
*/
@PostMapping("/login")
public Result<TokenInfoDTO> login(@RequestBody @Valid LoginDTO loginDTO) {
return authUserService.login(loginDTO);
}
/**
* 用户注册
*/
@PostMapping("/register")
public Result<String> register(@RequestBody @Valid RegisterDTO registerDTO) {
return authUserService.register(registerDTO);
}
/**
* 生成验证码
*/
@GetMapping("/captcha")
public Result<Map<String, Object>> captcha() {
return captchaService.generateCaptcha();
}
/**
* 刷新令牌
*/
@PostMapping("/refresh")
public Result<TokenInfoDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
return authUserService.refreshToken(refreshToken);
}
/**
* 用户登出
*/
@PostMapping("/logout")
public Result<Void> logout(HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return authUserService.logout(userId);
}
/**
* 获取当前用户信息
*/
@GetMapping("/current")
public Result<AuthUser> currentUser(HttpServletRequest request) {
Long userId = getUserIdFromRequest(request);
if (userId == null) {
return Result.error("未获取到用户ID");
}
return authUserService.getCurrentUser(userId);
}
/**
* 从请求中获取用户ID
*/
private Long getUserIdFromRequest(HttpServletRequest request) {
// 首先尝试从请求属性中获取
Object userIdAttr = request.getAttribute("userId");
if (userIdAttr != null) {
return (Long) userIdAttr;
}
// 如果请求属性中没有,尝试从认证上下文获取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getDetails() instanceof Long) {
return (Long) authentication.getDetails();
}
// 最后尝试从请求头获取(如果前端直接传递)
String userIdHeader = request.getHeader("userId");
if (userIdHeader != null && !userIdHeader.isEmpty()) {
try {
return Long.parseLong(userIdHeader);
} catch (NumberFormatException e) {
log.warn("Invalid userId header: {}", userIdHeader);
}
}
return null;
}
/**
* 根据ID获取用户名
*/
@GetMapping("/username/{userId}")
public String getUsernameById(@PathVariable Long userId) {
AuthUser user = authUserService.getById(userId);
return user != null ? user.getUsername() : null;
}
}
@@ -0,0 +1,36 @@
package com.nuaa.video.auth.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 登录请求DTO
*/
@Data
public class LoginDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
/**
* 验证码
*/
private String captcha;
/**
* 验证码KEY
*/
private String captchaKey;
}
@@ -0,0 +1,60 @@
package com.nuaa.video.auth.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serializable;
/**
* 注册请求DTO
*/
@Data
public class RegisterDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Pattern(regexp = "^[a-zA-Z0-9_]{4,16}$", message = "用户名必须为4-16位字母、数字或下划线")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^.{6,20}$", message = "密码长度必须为6-20位")
private String password;
/**
* 确认密码
*/
@NotBlank(message = "确认密码不能为空")
private String confirmPassword;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
private String email;
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 验证码
*/
@NotBlank(message = "验证码不能为空")
private String captcha;
/**
* 验证码KEY
*/
@NotBlank(message = "验证码KEY不能为空")
private String captchaKey;
}
@@ -0,0 +1,45 @@
package com.nuaa.video.auth.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* Token信息DTO
*/
@Data
@Accessors(chain = true)
public class TokenInfoDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 访问令牌
*/
private String accessToken;
/**
* 刷新令牌
*/
private String refreshToken;
/**
* 令牌类型
*/
private String tokenType = "Bearer";
/**
* 过期时间(秒)
*/
private long expiresIn;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
}
@@ -0,0 +1,59 @@
package com.nuaa.video.auth.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 权限实体
*/
@Data
@TableName("auth_permission")
public class AuthPermission implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 权限ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 权限名称
*/
private String name;
/**
* 权限编码
*/
private String code;
/**
* 权限描述
*/
private String description;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,59 @@
package com.nuaa.video.auth.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 角色实体
*/
@Data
@TableName("auth_role")
public class AuthRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 角色编码
*/
private String code;
/**
* 角色描述
*/
private String description;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.auth.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 角色权限关联实体
*/
@Data
@TableName("auth_role_permission")
public class AuthRolePermission implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 角色ID
*/
private Long roleId;
/**
* 权限ID
*/
private Long permissionId;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,91 @@
package com.nuaa.video.auth.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户认证实体
*/
@Data
@TableName("auth_user")
public class AuthUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 加密盐值
*/
private String salt;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phone;
/**
* 状态(0:禁用,1:正常)
*/
private Integer status;
/**
* 最后登录时间
*/
private LocalDateTime lastLoginTime;
/**
* 最后登录IP
*/
private String lastLoginIp;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 拥有的角色列表(非数据库字段)
*/
@TableField(exist = false)
private String[] roles;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.auth.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 用户角色关联实体
*/
@Data
@TableName("auth_user_role")
public class AuthUserRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 角色ID
*/
private Long roleId;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,15 @@
package com.nuaa.video.auth.feign;
import com.nuaa.video.auth.feign.fallback.UserClientFallback;
import com.nuaa.video.common.model.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "video-user", fallback = UserClientFallback.class)
public interface UserClient {
@PostMapping("/user/profile/init")
Result<Void> createUserInfo(@RequestParam("userId") Long userId,
@RequestParam("username") String username);
}
@@ -0,0 +1,17 @@
package com.nuaa.video.auth.feign.fallback;
import com.nuaa.video.auth.feign.UserClient;
import com.nuaa.video.common.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class UserClientFallback implements UserClient {
@Override
public Result<Void> createUserInfo(Long userId, String username) {
log.error("创建用户资料失败(回退): userId={}, username={}", userId, username);
return Result.error("用户服务不可用,资料将在首次访问时创建");
}
}
@@ -0,0 +1,84 @@
package com.nuaa.video.auth.filter;
import com.nuaa.video.auth.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* JWT认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private static final String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 从请求头中获取JWT令牌
String token = getJwtFromRequest(request);
// 如果令牌存在且有效,则设置认证信息
if (StringUtils.hasText(token)) {
try {
// 验证令牌并提取用户ID
Long userId = jwtUtils.getUserIdFromToken(token);
String username = jwtUtils.getUsernameFromToken(token);
if (userId != null && !jwtUtils.isTokenExpired(token)) {
log.debug("JWT验证成功, userId={}, username={}", userId, username);
// 设置用户权限(这里使用空列表,实际应用中可以从令牌或数据库获取)
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 创建认证对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
// 设置额外信息
authentication.setDetails(userId);
// 设置认证信息到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 在请求属性中存储用户ID,供控制器使用
request.setAttribute("userId", userId);
}
} catch (Exception e) {
log.error("JWT验证失败: {}", e.getMessage());
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(request, response);
}
/**
* 从请求头中提取JWT令牌
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
return bearerToken.substring(TOKEN_PREFIX.length());
}
return null;
}
}
@@ -0,0 +1,12 @@
package com.nuaa.video.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.auth.entity.AuthPermission;
import org.apache.ibatis.annotations.Mapper;
/**
* 权限Mapper接口
*/
@Mapper
public interface AuthPermissionMapper extends BaseMapper<AuthPermission> {
}
@@ -0,0 +1,12 @@
package com.nuaa.video.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.auth.entity.AuthRole;
import org.apache.ibatis.annotations.Mapper;
/**
* 角色Mapper接口
*/
@Mapper
public interface AuthRoleMapper extends BaseMapper<AuthRole> {
}
@@ -0,0 +1,12 @@
package com.nuaa.video.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.auth.entity.AuthRolePermission;
import org.apache.ibatis.annotations.Mapper;
/**
* 角色权限关联Mapper接口
*/
@Mapper
public interface AuthRolePermissionMapper extends BaseMapper<AuthRolePermission> {
}
@@ -0,0 +1,31 @@
package com.nuaa.video.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.auth.entity.AuthUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户Mapper接口
*/
@Mapper
public interface AuthUserMapper extends BaseMapper<AuthUser> {
/**
* 获取用户角色
*
* @param userId 用户ID
* @return 角色编码列表
*/
List<String> getUserRoles(@Param("userId") Long userId);
/**
* 获取用户权限
*
* @param userId 用户ID
* @return 权限编码列表
*/
List<String> getUserPermissions(@Param("userId") Long userId);
}
@@ -0,0 +1,12 @@
package com.nuaa.video.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.auth.entity.AuthUserRole;
import org.apache.ibatis.annotations.Mapper;
/**
* 用户角色关联Mapper接口
*/
@Mapper
public interface AuthUserRoleMapper extends BaseMapper<AuthUserRole> {
}
@@ -0,0 +1,77 @@
package com.nuaa.video.auth.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import com.nuaa.video.auth.dto.TokenInfoDTO;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.common.model.vo.Result;
/**
* 用户认证服务接口
*/
public interface AuthUserService extends IService<AuthUser> {
/**
* 用户登录
*
* @param loginDTO 登录信息
* @return 登录结果
*/
Result<TokenInfoDTO> login(LoginDTO loginDTO);
/**
* 用户注册
*
* @param registerDTO 注册信息
* @return 注册结果
*/
Result<String> register(RegisterDTO registerDTO);
/**
* 通过用户名查询用户
*
* @param username 用户名
* @return 用户信息
*/
AuthUser getByUsername(String username);
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return 新的令牌信息
*/
Result<TokenInfoDTO> refreshToken(String refreshToken);
/**
* 登出
*
* @param userId 用户ID
* @return 登出结果
*/
Result<Void> logout(Long userId);
/**
* 获取当前登录用户信息
*
* @param userId 用户ID
* @return 用户信息
*/
Result<AuthUser> getCurrentUser(Long userId);
/**
* 获取登录统计 - 管理接口
*/
Result<Object> getLoginStats();
/**
* 管理员重置用户密码 - 管理接口
*/
Result<Void> adminResetPassword(Long userId, String newPassword);
/**
* 锁定/解锁用户账号 - 管理接口
*/
Result<Void> lockUser(Long userId, Boolean locked);
}
@@ -0,0 +1,18 @@
package com.nuaa.video.auth.service;
import com.nuaa.video.common.model.vo.Result;
import java.util.Map;
/**
* 验证码服务接口
*/
public interface CaptchaService {
/**
* 生成验证码
*
* @return 验证码信息
*/
Result<Map<String, Object>> generateCaptcha();
}
@@ -0,0 +1,452 @@
package com.nuaa.video.auth.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import com.nuaa.video.auth.dto.TokenInfoDTO;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.entity.AuthUserRole;
import com.nuaa.video.auth.feign.UserClient;
import com.nuaa.video.auth.mapper.AuthUserMapper;
import com.nuaa.video.auth.mapper.AuthUserRoleMapper;
import com.nuaa.video.auth.service.AuthUserService;
import com.nuaa.video.common.model.enums.ResultCode;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.auth.utils.JwtUtils;
import com.nuaa.video.auth.utils.RedisUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户认证服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor //Lombok注解,会自动为final字段创建构造函数
public class AuthUserServiceImpl extends ServiceImpl<AuthUserMapper, AuthUser> implements AuthUserService {
private final AuthUserMapper authUserMapper;
private final AuthUserRoleMapper authUserRoleMapper;
private final PasswordEncoder passwordEncoder;
private final JwtUtils jwtUtils;
private final RedisUtils redisUtils;
private final UserClient userClient;
@Value("${jwt.expiration:86400}")
private long expiration;
private static final String USER_TOKEN_PREFIX = "user:token:";
private static final String CAPTCHA_PREFIX = "captcha:";
private static final String USER_ROLE_PREFIX = "user:role:";
private static final String USER_PERMISSION_PREFIX = "user:permission:";
@Override
public Result<TokenInfoDTO> login(LoginDTO loginDTO) {
// 验证验证码
if (StringUtils.hasText(loginDTO.getCaptchaKey()) && StringUtils.hasText(loginDTO.getCaptcha())) {
String captchaCache = (String) redisUtils.get(CAPTCHA_PREFIX + loginDTO.getCaptchaKey());
if (captchaCache == null) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码已过期");
}
if (!loginDTO.getCaptcha().equalsIgnoreCase(captchaCache)) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码错误");
}
// 验证通过后移除验证码
redisUtils.del(CAPTCHA_PREFIX + loginDTO.getCaptchaKey());
}
// 查询用户
AuthUser user = getByUsername(loginDTO.getUsername());
if (user == null) {
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), "用户名或密码错误");
}
// 校验密码
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
return Result.error(ResultCode.PASSWORD_ERROR.getCode(), "用户名或密码错误");
}
// 检查用户状态
if (user.getStatus() != 1) {
return Result.error(ResultCode.FORBIDDEN.getCode(), "账号已被禁用");
}
// 更新登录信息
user.setLastLoginTime(LocalDateTime.now());
updateById(user);
// 生成令牌
String accessToken = jwtUtils.generateToken(user.getId(), user.getUsername());
String refreshToken = RandomUtil.randomString(32);
// 存储刷新令牌
redisUtils.set(USER_TOKEN_PREFIX + user.getId(), refreshToken, expiration * 2);
// 缓存用户角色和权限
cacheUserAuthInfo(user.getId());
// 构建返回结果
TokenInfoDTO tokenInfo = new TokenInfoDTO()
.setAccessToken(accessToken)
.setRefreshToken(refreshToken)
.setTokenType("Bearer")
.setExpiresIn(expiration)
.setUserId(user.getId())
.setUsername(user.getUsername());
return Result.success(tokenInfo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<String> register(RegisterDTO registerDTO) {
// 验证验证码
String captchaCache = (String) redisUtils.get(CAPTCHA_PREFIX + registerDTO.getCaptchaKey());
if (captchaCache == null) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码已过期");
}
if (!registerDTO.getCaptcha().equalsIgnoreCase(captchaCache)) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码错误");
}
// 验证两次密码输入是否一致
if (!registerDTO.getPassword().equals(registerDTO.getConfirmPassword())) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "两次密码输入不一致");
}
// 验证用户名是否已存在
AuthUser existUser = getByUsername(registerDTO.getUsername());
if (existUser != null) {
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "用户名已存在");
}
// 验证邮箱是否已存在
if (StringUtils.hasText(registerDTO.getEmail())) {
Long count = count(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getEmail, registerDTO.getEmail()));
if (count > 0) {
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "邮箱已被注册");
}
}
// 验证手机号是否已存在
if (StringUtils.hasText(registerDTO.getPhone())) {
Long count = count(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getPhone, registerDTO.getPhone()));
if (count > 0) {
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "手机号已被注册");
}
}
// 移除验证码
redisUtils.del(CAPTCHA_PREFIX + registerDTO.getCaptchaKey());
// 创建用户
AuthUser user = new AuthUser();
user.setUsername(registerDTO.getUsername());
user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
user.setSalt(RandomUtil.randomString(6));
user.setEmail(registerDTO.getEmail());
user.setPhone(registerDTO.getPhone());
user.setStatus(1);
user.setCreatedBy("system");
user.setUpdatedBy("system");
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 保存用户
save(user);
// 分配默认角色(普通用户)
AuthUserRole userRole = new AuthUserRole();
userRole.setUserId(user.getId());
userRole.setRoleId(2L); // 普通用户角色ID
userRole.setCreatedBy("system");
userRole.setCreateTime(LocalDateTime.now());
authUserRoleMapper.insert(userRole);
// 使用事务同步器确保在事务提交后再创建用户资料
final Long userId = user.getId();
final String username = user.getUsername();
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
try {
userClient.createUserInfo(userId, username);
log.info("用户资料创建成功:userId={}", userId);
} catch (Exception e) {
log.error("创建用户资料失败,用户ID: " + userId, e);
}
}
});
return Result.success("注册成功");
}
@Override
public AuthUser getByUsername(String username) {
return getOne(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getUsername, username));
}
@Override
public Result<TokenInfoDTO> refreshToken(String refreshToken) {
try {
if (!StringUtils.hasText(refreshToken)) {
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌无效");
}
// 解析accessToken获取用户ID
String accessToken = jwtUtils.getUserIdFromToken(refreshToken).toString();
Long userId = Long.parseLong(accessToken);
// 验证refreshToken是否存在且匹配
String cacheRefreshToken = (String) redisUtils.get(USER_TOKEN_PREFIX + userId);
if (cacheRefreshToken == null || !cacheRefreshToken.equals(refreshToken)) {
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌无效");
}
// 获取用户信息
AuthUser user = getById(userId);
if (user == null || user.getStatus() != 1) {
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "用户不存在或已被禁用");
}
// 生成新令牌
String newAccessToken = jwtUtils.generateToken(user.getId(), user.getUsername());
String newRefreshToken = RandomUtil.randomString(32);
// 更新刷新令牌
redisUtils.set(USER_TOKEN_PREFIX + user.getId(), newRefreshToken, expiration * 2);
// 构建返回结果
TokenInfoDTO tokenInfo = new TokenInfoDTO()
.setAccessToken(newAccessToken)
.setRefreshToken(newRefreshToken)
.setTokenType("Bearer")
.setExpiresIn(expiration)
.setUserId(user.getId())
.setUsername(user.getUsername());
return Result.success(tokenInfo);
} catch (Exception e) {
log.error("刷新令牌失败", e);
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌失败");
}
}
@Override
public Result<Void> logout(Long userId) {
if (userId == null) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "用户ID不能为空");
}
// 清除用户令牌
redisUtils.del(USER_TOKEN_PREFIX + userId);
// 清除用户权限缓存
redisUtils.del(USER_ROLE_PREFIX + userId);
redisUtils.del(USER_PERMISSION_PREFIX + userId);
return Result.success();
}
@Override
public Result<AuthUser> getCurrentUser(Long userId) {
if (userId == null) {
return Result.error(ResultCode.PARAM_ERROR.getCode(), "用户ID不能为空");
}
AuthUser user = getById(userId);
if (user == null) {
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), "用户不存在");
}
// 脱敏处理
user.setPassword(null);
user.setSalt(null);
// 获取角色信息
List<String> roles = getUserRoles(userId);
user.setRoles(roles.toArray(new String[0]));
return Result.success(user);
}
@Override
public Result<Object> getLoginStats() {
Map<String, Object> stats = new HashMap<>();
// 这里是模拟数据,实际项目中需要从数据库或缓存中获取
// 总用户数
long totalUsers = count();
stats.put("totalUsers", totalUsers);
// 今日登录用户数
LocalDate today = LocalDate.now();
long todayLoginUsers = 0; // 实际需要查询登录记录表
stats.put("todayLoginUsers", todayLoginUsers);
// 当前在线用户数(可从Redis中获取)
long onlineUsers = 0; // 实际需要从Redis或其他存储中获取
stats.put("onlineUsers", onlineUsers);
// 最近7天每日登录用户数
Map<String, Long> dailyLoginUsers = new HashMap<>();
for (int i = 6; i >= 0; i--) {
LocalDate date = today.minusDays(i);
String dateStr = date.toString();
long count = 0; // 实际需要查询登录记录表
dailyLoginUsers.put(dateStr, count);
}
stats.put("dailyLoginUsers", dailyLoginUsers);
return Result.success(stats);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> adminResetPassword(Long userId, String newPassword) {
// 查询用户是否存在
AuthUser user = getById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 生成新的盐值
String salt = generateSalt();
// 加密密码
String encryptedPassword = encryptPassword(newPassword, salt);
// 更新密码和盐值
AuthUser updateUser = new AuthUser();
updateUser.setId(userId);
updateUser.setPassword(encryptedPassword);
updateUser.setSalt(salt);
updateUser.setUpdatedBy("admin");
updateUser.setUpdateTime(LocalDateTime.now());
updateById(updateUser);
// 清除用户token
String tokenKey = "auth:token:" + userId;
redisUtils.del(tokenKey);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> lockUser(Long userId, Boolean locked) {
// 查询用户是否存在
AuthUser user = getById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 更新用户状态
AuthUser updateUser = new AuthUser();
updateUser.setId(userId);
updateUser.setStatus(locked ? 0 : 1); // 0-禁用,1-正常
updateUser.setUpdatedBy("admin");
updateUser.setUpdateTime(LocalDateTime.now());
updateById(updateUser);
// 如果锁定用户,清除用户token
if (locked) {
String tokenKey = "auth:token:" + userId;
redisUtils.del(tokenKey);
}
return Result.success();
}
/**
* 生成盐值
*/
private String generateSalt() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 加密密码
*/
private String encryptPassword(String password, String salt) {
// 实际项目中应该使用加密算法如BCrypt
return DigestUtils.md5DigestAsHex((password + salt).getBytes());
}
/**
* 获取用户角色
*
* @param userId 用户ID
* @return 角色列表
*/
private List<String> getUserRoles(Long userId) {
// 优先从缓存获取
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) redisUtils.get(USER_ROLE_PREFIX + userId);
if (roles == null || roles.isEmpty()) {
// 从数据库查询并缓存
roles = authUserMapper.getUserRoles(userId);
if (!roles.isEmpty()) {
redisUtils.set(USER_ROLE_PREFIX + userId, roles, expiration);
}
}
return roles;
}
/**
* 获取用户权限
*
* @param userId 用户ID
* @return 权限列表
*/
private List<String> getUserPermissions(Long userId) {
// 优先从缓存获取
@SuppressWarnings("unchecked")
List<String> permissions = (List<String>) redisUtils.get(USER_PERMISSION_PREFIX + userId);
if (permissions == null || permissions.isEmpty()) {
// 从数据库查询并缓存
permissions = authUserMapper.getUserPermissions(userId);
if (!permissions.isEmpty()) {
redisUtils.set(USER_PERMISSION_PREFIX + userId, permissions, expiration);
}
}
return permissions;
}
/**
* 缓存用户认证信息
*
* @param userId 用户ID
*/
private void cacheUserAuthInfo(Long userId) {
// 缓存用户角色
List<String> roles = authUserMapper.getUserRoles(userId);
if (!roles.isEmpty()) {
redisUtils.set(USER_ROLE_PREFIX + userId, roles, expiration);
}
// 缓存用户权限
List<String> permissions = authUserMapper.getUserPermissions(userId);
if (!permissions.isEmpty()) {
redisUtils.set(USER_PERMISSION_PREFIX + userId, permissions, expiration);
}
}
}
@@ -0,0 +1,65 @@
package com.nuaa.video.auth.service.impl;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import com.nuaa.video.auth.service.CaptchaService;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.auth.utils.RedisUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
/**
* 验证码服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CaptchaServiceImpl implements CaptchaService {
private final RedisUtils redisUtils;
@Value("${captcha.expiration:60}")
private long captchaExpiration;
private static final String CAPTCHA_PREFIX = "captcha:";
@Override
public Result<Map<String, Object>> generateCaptcha() {
try {
// 创建验证码
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 40, 4, 10);
// 获取验证码内容和图片
String code = captcha.getCode();
// 生成验证码key
String captchaKey = RandomUtil.randomString(16);
// 将验证码存入Redis
redisUtils.set(CAPTCHA_PREFIX + captchaKey, code, captchaExpiration);
// 转换验证码图片为Base64
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
captcha.write(outputStream);
String captchaBase64 = "data:image/png;base64," + Base64.encode(outputStream.toByteArray());
// 构建返回信息
Map<String, Object> result = new HashMap<>();
result.put("captchaKey", captchaKey);
result.put("captchaImage", captchaBase64);
return Result.success(result);
} catch (Exception e) {
log.error("生成验证码失败", e);
return Result.error("生成验证码失败");
}
}
}
@@ -0,0 +1,124 @@
package com.nuaa.video.auth.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类 - auth模块专用
*/
@Slf4j
@Component
public class JwtUtils {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
private String secret;
@Value("${jwt.expiration:86400}")
private long expiration;
@Value("${jwt.token-header:Authorization}")
private String tokenHeader;
/**
* 根据用户ID生成JWT
*/
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return createToken(claims, userId.toString());
}
/**
* 从JWT中获取用户ID
*/
public Long getUserIdFromToken(String token) {
return Long.valueOf(getClaimFromToken(token, Claims::getSubject));
}
/**
* 判断JWT是否过期
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 验证JWT
*/
public boolean validateToken(String token, String username) {
try {
final String tokenUsername = getUsernameFromToken(token);
return (tokenUsername.equals(username) && !isTokenExpired(token));
} catch (Exception e) {
log.error("JWT验证失败:", e);
return false;
}
}
/**
* 从JWT中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("username", String.class));
}
/**
* 获取JWT过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 从JWT中获取指定声明
*/
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 从JWT中获取所有声明
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 创建JWT
*/
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getSigningKey())
.compact();
}
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
}
@@ -0,0 +1,105 @@
package com.nuaa.video.auth.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具类 - auth模块专用
*/
@Component
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 设置缓存过期时间
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取缓存过期时间
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*/
public boolean hasKey(String key) {
try {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
} catch (Exception e) {
return false;
}
}
/**
* 删除缓存
*/
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete((Collection<String>) List.of(key));
}
}
}
/**
* 获取普通缓存
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 设置普通缓存
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 设置普通缓存并设置过期时间
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
return false;
}
}
// 其他必要的方法可以根据需要添加
}
@@ -0,0 +1,55 @@
server:
port: 9001
spring:
application:
name: video-auth
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
sentinel:
transport:
dashboard: ${SENTINEL_HOST:sentinel}:${SENTINEL_PORT:8080}
port: 8721
eager: true
datasource:
url: jdbc:mysql://${MYSQL_HOST:mysql}:${MYSQL_PORT:3306}/video_auth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
host: ${REDIS_HOST:redis}
port: ${REDIS_PORT:6379}
password: redis_EsJ8FX
database: 0
timeout: 10000
lettuce:
pool:
max-active: 200
max-wait: -1
max-idle: 10
min-idle: 0
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.nuaa.video.auth.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: is_deleted
logic-delete-value: 1
logic-not-delete-value: 0
jwt:
secret: your-secret-key-here-should-be-very-long-and-secure
expiration: 86400000 # 24小时
header: Authorization
tokenPrefix: Bearer
@@ -0,0 +1,38 @@
server:
port: 9001
spring:
application:
name: video-auth
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
config:
import:
- optional:nacos:common.yaml?refreshEnabled=true
- optional:nacos:common-redis-${spring.profiles.active}.yaml?refreshEnabled=true
- optional:nacos:${spring.application.name}.yaml?refreshEnabled=true
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
ip: ${SPRING_CLOUD_NACOS_DISCOVERY_IP:}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
@@ -0,0 +1,23 @@
spring:
application:
name: video-auth
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
ephemeral: true
ip: ${AUTH_IP:}
port: ${server.port:9001}
fail-fast: true
register-enabled: true
config:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
file-extension: yaml
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.nuaa.video.auth.mapper.AuthPermissionMapper">
</mapper>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.nuaa.video.auth.mapper.AuthRoleMapper">
</mapper>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.nuaa.video.auth.mapper.AuthRolePermissionMapper">
</mapper>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.auth.mapper.AuthUserMapper">
<!-- 获取用户角色 -->
<select id="getUserRoles" resultType="string">
SELECT
r.code
FROM
auth_user_role ur
INNER JOIN auth_role r ON ur.role_id = r.id
WHERE
ur.user_id = #{userId}
</select>
<!-- 获取用户权限 -->
<select id="getUserPermissions" resultType="string">
SELECT
p.code
FROM
auth_user_role ur
INNER JOIN auth_role_permission rp ON ur.role_id = rp.role_id
INNER JOIN auth_permission p ON rp.permission_id = p.id
WHERE
ur.user_id = #{userId}
</select>
</mapper>
@@ -0,0 +1,237 @@
package com.nuaa.video.auth.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import com.nuaa.video.auth.dto.TokenInfoDTO;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.service.AuthUserService;
import com.nuaa.video.auth.service.CaptchaService;
import com.nuaa.video.common.model.vo.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.HashMap;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* AuthController 单元测试
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("认证控制器测试")
class AuthControllerTest {
private MockMvc mockMvc;
@Mock
private AuthUserService authUserService;
@Mock
private CaptchaService captchaService;
@InjectMocks
private AuthController authController;
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(authController).build();
objectMapper = new ObjectMapper();
}
@Test
@DisplayName("登录接口 - 成功")
void testLoginSuccess() throws Exception {
// Given
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("testuser");
loginDTO.setPassword("password123");
TokenInfoDTO tokenInfo = new TokenInfoDTO();
tokenInfo.setAccessToken("accessToken123");
tokenInfo.setRefreshToken("refreshToken123");
tokenInfo.setTokenType("Bearer");
tokenInfo.setExpiresIn(86400L);
tokenInfo.setUserId(1L);
tokenInfo.setUsername("testuser");
when(authUserService.login(any(LoginDTO.class)))
.thenReturn(Result.success(tokenInfo));
// When & Then
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.accessToken").value("accessToken123"))
.andExpect(jsonPath("$.data.username").value("testuser"));
verify(authUserService, times(1)).login(any(LoginDTO.class));
}
@Test
@DisplayName("登录接口 - 用户不存在")
void testLoginFailUserNotFound() throws Exception {
// Given
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("nonexistent");
loginDTO.setPassword("password123");
when(authUserService.login(any(LoginDTO.class)))
.thenReturn(Result.error(404, "用户名或密码错误"));
// When & Then
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(404))
.andExpect(jsonPath("$.message").value("用户名或密码错误"));
}
@Test
@DisplayName("注册接口 - 成功")
void testRegisterSuccess() throws Exception {
// Given
RegisterDTO registerDTO = new RegisterDTO();
registerDTO.setUsername("newuser");
registerDTO.setPassword("password123");
registerDTO.setConfirmPassword("password123");
registerDTO.setEmail("newuser@example.com");
registerDTO.setPhone("13900139000");
registerDTO.setCaptchaKey("captchaKey123");
registerDTO.setCaptcha("1234");
when(authUserService.register(any(RegisterDTO.class)))
.thenReturn(Result.success("注册成功"));
// When & Then
mockMvc.perform(post("/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value("注册成功"));
verify(authUserService, times(1)).register(any(RegisterDTO.class));
}
@Test
@DisplayName("注册接口 - 用户名已存在")
void testRegisterFailUsernameExists() throws Exception {
// Given
RegisterDTO registerDTO = new RegisterDTO();
registerDTO.setUsername("existinguser");
registerDTO.setPassword("password123");
registerDTO.setConfirmPassword("password123");
when(authUserService.register(any(RegisterDTO.class)))
.thenReturn(Result.error(400, "用户名已存在"));
// When & Then
mockMvc.perform(post("/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(400))
.andExpect(jsonPath("$.message").value("用户名已存在"));
}
@Test
@DisplayName("获取验证码接口")
void testGetCaptcha() throws Exception {
// Given
Map<String, Object> captchaData = new HashMap<>();
captchaData.put("captchaKey", "key123");
captchaData.put("captchaImage", "base64ImageData");
when(captchaService.generateCaptcha())
.thenReturn(Result.success(captchaData));
// When & Then
mockMvc.perform(get("/auth/captcha"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.captchaKey").value("key123"));
verify(captchaService, times(1)).generateCaptcha();
}
@Test
@DisplayName("刷新令牌接口 - 成功")
void testRefreshTokenSuccess() throws Exception {
// Given
String refreshToken = "validRefreshToken";
TokenInfoDTO tokenInfo = new TokenInfoDTO();
tokenInfo.setAccessToken("newAccessToken");
tokenInfo.setRefreshToken("newRefreshToken");
tokenInfo.setTokenType("Bearer");
tokenInfo.setExpiresIn(86400L);
when(authUserService.refreshToken(refreshToken))
.thenReturn(Result.success(tokenInfo));
// When & Then
mockMvc.perform(post("/auth/refresh")
.param("refreshToken", refreshToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.accessToken").value("newAccessToken"));
verify(authUserService, times(1)).refreshToken(refreshToken);
}
@Test
@DisplayName("获取当前用户信息接口 - 成功")
void testGetCurrentUserSuccess() throws Exception {
// Given
AuthUser user = new AuthUser();
user.setId(1L);
user.setUsername("testuser");
user.setEmail("test@example.com");
user.setStatus(1);
when(authUserService.getCurrentUser(1L))
.thenReturn(Result.success(user));
// When & Then
mockMvc.perform(get("/auth/current")
.header("userId", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200));
}
@Test
@DisplayName("根据ID获取用户名接口")
void testGetUsernameById() throws Exception {
// Given
AuthUser user = new AuthUser();
user.setId(1L);
user.setUsername("testuser");
when(authUserService.getById(1L)).thenReturn(user);
// When & Then
mockMvc.perform(get("/auth/username/1"))
.andExpect(status().isOk())
.andExpect(content().string("testuser"));
verify(authUserService, times(1)).getById(1L);
}
}
@@ -0,0 +1,64 @@
package com.nuaa.video.auth.feign;
import com.nuaa.video.auth.feign.fallback.UserClientFallback;
import com.nuaa.video.common.model.vo.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
/**
* UserClient Feign客户端测试
* 测试服务降级和远程调用逻辑
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("UserClient Feign客户端测试")
class UserClientTest {
@InjectMocks
private UserClientFallback userClientFallback;
@Test
@DisplayName("创建用户信息 - 服务降级测试")
void testCreateUserInfoFallback() {
// Given
Long userId = 1L;
String username = "testuser";
// When
Result<Void> result = userClientFallback.createUserInfo(userId, username);
// Then
assertNotNull(result);
assertEquals(500, result.getCode());
assertEquals("创建用户资料失败,用户服务不可用", result.getMessage());
}
@Test
@DisplayName("测试参数校验 - 用户ID为空")
void testCreateUserInfoWithNullUserId() {
// Given
Long userId = null;
String username = "testuser";
// Then - 参数校验应该在调用方进行
assertNull(userId);
assertNotNull(username);
}
@Test
@DisplayName("测试参数校验 - 用户名为空")
void testCreateUserInfoWithNullUsername() {
// Given
Long userId = 1L;
String username = null;
// Then - 参数校验应该在调用方进行
assertNotNull(userId);
assertNull(username);
}
}
@@ -0,0 +1,92 @@
package com.nuaa.video.auth.integration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* 认证服务集成测试
* 需要启动完整的Spring上下文
*
* 注意:此测试需要以下环境:
* 1. MySQL数据库
* 2. Redis服务
* 3. Nacos服务(可禁用)
*
* 运行前请确保测试环境配置正确
*/
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@DisplayName("认证服务集成测试")
class AuthIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("集成测试 - 获取验证码")
void testGetCaptcha() throws Exception {
mockMvc.perform(get("/auth/captcha"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.captchaKey").exists())
.andExpect(jsonPath("$.data.captchaImage").exists());
}
@Test
@DisplayName("集成测试 - 用户登录失败(用户不存在)")
void testLoginWithNonExistentUser() throws Exception {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("nonexistentuser");
loginDTO.setPassword("wrongpassword");
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(404));
}
@Test
@DisplayName("集成测试 - 用户注册失败(验证码错误)")
void testRegisterWithInvalidCaptcha() throws Exception {
RegisterDTO registerDTO = new RegisterDTO();
registerDTO.setUsername("newuser");
registerDTO.setPassword("password123");
registerDTO.setConfirmPassword("password123");
registerDTO.setEmail("newuser@example.com");
registerDTO.setPhone("13800138000");
registerDTO.setCaptchaKey("invalidKey");
registerDTO.setCaptcha("wrong");
mockMvc.perform(post("/auth/register")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(registerDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(400));
}
@Test
@DisplayName("集成测试 - 刷新令牌失败(无效令牌)")
void testRefreshTokenWithInvalidToken() throws Exception {
mockMvc.perform(post("/auth/refresh")
.param("refreshToken", "invalidToken"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(401));
}
}
@@ -0,0 +1,263 @@
package com.nuaa.video.auth.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.nuaa.video.auth.dto.LoginDTO;
import com.nuaa.video.auth.dto.RegisterDTO;
import com.nuaa.video.auth.dto.TokenInfoDTO;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.entity.AuthUserRole;
import com.nuaa.video.auth.feign.UserClient;
import com.nuaa.video.auth.mapper.AuthUserMapper;
import com.nuaa.video.auth.mapper.AuthUserRoleMapper;
import com.nuaa.video.auth.service.impl.AuthUserServiceImpl;
import com.nuaa.video.auth.utils.JwtUtils;
import com.nuaa.video.auth.utils.RedisUtils;
import com.nuaa.video.common.model.vo.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.time.LocalDateTime;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* AuthUserService 单元测试
*/
@ExtendWith(MockitoExtension.class)
@DisplayName("认证服务测试")
class AuthUserServiceTest {
@Mock
private AuthUserMapper authUserMapper;
@Mock
private AuthUserRoleMapper authUserRoleMapper;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtUtils jwtUtils;
@Mock
private RedisUtils redisUtils;
@Mock
private UserClient userClient;
@InjectMocks
private AuthUserServiceImpl authUserService;
private AuthUser testUser;
private LoginDTO loginDTO;
private RegisterDTO registerDTO;
@BeforeEach
void setUp() {
// 初始化测试用户
testUser = new AuthUser();
testUser.setId(1L);
testUser.setUsername("testuser");
testUser.setPassword("$2a$10$encryptedPassword");
testUser.setEmail("test@example.com");
testUser.setPhone("13800138000");
testUser.setStatus(1);
testUser.setCreateTime(LocalDateTime.now());
testUser.setUpdateTime(LocalDateTime.now());
// 初始化登录DTO
loginDTO = new LoginDTO();
loginDTO.setUsername("testuser");
loginDTO.setPassword("password123");
// 初始化注册DTO
registerDTO = new RegisterDTO();
registerDTO.setUsername("newuser");
registerDTO.setPassword("password123");
registerDTO.setConfirmPassword("password123");
registerDTO.setEmail("newuser@example.com");
registerDTO.setPhone("13900139000");
registerDTO.setCaptchaKey("testKey");
registerDTO.setCaptcha("1234");
}
@Test
@DisplayName("登录成功 - 正确的用户名和密码")
void testLoginSuccess() {
// Given
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(testUser);
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
when(jwtUtils.generateToken(anyLong(), anyString())).thenReturn("accessToken123");
when(authUserMapper.getUserRoles(anyLong())).thenReturn(Arrays.asList("USER_NORMAL"));
when(authUserMapper.getUserPermissions(anyLong())).thenReturn(Arrays.asList("video:upload"));
when(redisUtils.set(anyString(), any(), anyLong())).thenReturn(true);
when(authUserMapper.updateById(any(AuthUser.class))).thenReturn(1);
// 验证mock设置
assertNotNull(testUser);
assertEquals("testuser", testUser.getUsername());
assertEquals(1, testUser.getStatus());
}
@Test
@DisplayName("登录失败 - 用户不存在")
void testLoginFailUserNotFound() {
// Given
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
// 验证用户为空
AuthUser user = authUserMapper.selectOne(mock(LambdaQueryWrapper.class));
assertNull(user);
}
@Test
@DisplayName("登录失败 - 密码错误")
void testLoginFailWrongPassword() {
// Given
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(testUser);
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
// 验证密码匹配失败
boolean matches = passwordEncoder.matches("wrongpassword", testUser.getPassword());
assertFalse(matches);
}
@Test
@DisplayName("登录失败 - 账号被禁用")
void testLoginFailUserDisabled() {
// Given
testUser.setStatus(0); // 禁用状态
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(testUser);
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
// 验证用户状态
assertNotEquals(1, testUser.getStatus());
}
@Test
@DisplayName("注册成功")
void testRegisterSuccess() {
// Given
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
when(authUserMapper.selectCount(any(LambdaQueryWrapper.class))).thenReturn(0L);
when(passwordEncoder.encode(anyString())).thenReturn("$2a$10$encryptedPassword");
when(authUserMapper.insert(any(AuthUser.class))).thenReturn(1);
when(authUserRoleMapper.insert(any(AuthUserRole.class))).thenReturn(1);
// 验证DTO数据正确
assertEquals("newuser", registerDTO.getUsername());
assertEquals(registerDTO.getPassword(), registerDTO.getConfirmPassword());
}
@Test
@DisplayName("注册失败 - 用户名已存在")
void testRegisterFailUsernameExists() {
// Given
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(testUser);
// 验证用户已存在
AuthUser existingUser = authUserMapper.selectOne(mock(LambdaQueryWrapper.class));
assertNotNull(existingUser);
assertEquals("testuser", existingUser.getUsername());
}
@Test
@DisplayName("注册失败 - 两次密码不一致")
void testRegisterFailPasswordMismatch() {
// Given
registerDTO.setConfirmPassword("differentPassword");
// 验证密码不匹配
assertNotEquals(registerDTO.getPassword(), registerDTO.getConfirmPassword());
}
@Test
@DisplayName("刷新令牌成功")
void testRefreshTokenSuccess() {
// Given
String refreshToken = "validRefreshToken";
when(redisUtils.get(anyString())).thenReturn(refreshToken);
when(jwtUtils.getUserIdFromToken(anyString())).thenReturn(1L);
when(authUserMapper.selectById(1L)).thenReturn(testUser);
when(jwtUtils.generateToken(anyLong(), anyString())).thenReturn("newAccessToken");
// 验证mock设置正确
assertNotNull(testUser);
assertEquals(1L, testUser.getId());
}
@Test
@DisplayName("登出成功")
void testLogoutSuccess() {
// Given
Long userId = 1L;
// 验证userId不为空
assertNotNull(userId);
// del方法返回void,无法mock返回值,但可以验证方法行为
// 实际测试中可以调用 doNothing().when(redisUtils).del(anyString())
}
@Test
@DisplayName("获取当前用户信息成功")
void testGetCurrentUserSuccess() {
// Given
Long userId = 1L;
when(authUserMapper.selectById(userId)).thenReturn(testUser);
when(authUserMapper.getUserRoles(userId)).thenReturn(Arrays.asList("USER_NORMAL"));
// When
AuthUser user = authUserMapper.selectById(userId);
// Then
assertNotNull(user);
assertEquals("testuser", user.getUsername());
}
@Test
@DisplayName("根据用户名查询用户")
void testGetByUsername() {
// Given
String username = "testuser";
when(authUserMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(testUser);
// When
AuthUser user = authUserMapper.selectOne(mock(LambdaQueryWrapper.class));
// Then
assertNotNull(user);
assertEquals(username, user.getUsername());
}
@Test
@DisplayName("检查用户状态 - 正常")
void testUserStatusNormal() {
// Given
testUser.setStatus(1);
// Then
assertEquals(1, testUser.getStatus());
assertTrue(testUser.getStatus() == 1);
}
@Test
@DisplayName("检查用户状态 - 禁用")
void testUserStatusDisabled() {
// Given
testUser.setStatus(0);
// Then
assertEquals(0, testUser.getStatus());
assertFalse(testUser.getStatus() == 1);
}
}
@@ -0,0 +1,46 @@
spring:
application:
name: video-auth-test
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL;DATABASE_TO_LOWER=TRUE
username: sa
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
h2:
console:
enabled: true
data:
redis:
host: localhost
port: 6379
database: 0
# JWT配置
jwt:
secret: test-secret-key-for-unit-testing-purpose-only-12345678
expiration: 86400
# 日志配置
logging:
level:
com.nuaa.video: DEBUG
org.springframework: INFO
# Nacos配置(测试环境禁用)
spring:
cloud:
nacos:
discovery:
enabled: false
config:
enabled: false
# Sentinel配置(测试环境禁用)
spring:
cloud:
sentinel:
enabled: false