diff --git a/.idea/.cache/.Apifox_Helper/.toolWindow.db b/.idea/.cache/.Apifox_Helper/.toolWindow.db new file mode 100644 index 0000000..a5693ff Binary files /dev/null and b/.idea/.cache/.Apifox_Helper/.toolWindow.db differ diff --git a/.idea/ApifoxUploaderProjectSetting.xml b/.idea/ApifoxUploaderProjectSetting.xml index fc0729c..4c2b765 100644 --- a/.idea/ApifoxUploaderProjectSetting.xml +++ b/.idea/ApifoxUploaderProjectSetting.xml @@ -2,5 +2,12 @@ + \ No newline at end of file diff --git a/.idea/fastRequest/fastRequestCollection.xml b/.idea/fastRequest/fastRequestCollection.xml new file mode 100644 index 0000000..a433492 --- /dev/null +++ b/.idea/fastRequest/fastRequestCollection.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml b/.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml index e62f560..e03c3b9 100644 --- a/.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml +++ b/.idea/fastRequest/fastRequestCurrentProjectLocalConfig.xml @@ -1,6 +1,9 @@ + \ No newline at end of file diff --git a/docs/video-auth模块目录架构、功能及接口文档.md b/docs/video-auth模块目录架构、功能及接口文档.md new file mode 100644 index 0000000..9f59a0d --- /dev/null +++ b/docs/video-auth模块目录架构、功能及接口文档.md @@ -0,0 +1,269 @@ +# video-auth模块目录架构、功能及接口文档 + +## 一、目录架构 + +``` +video-auth/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/nuaa/video/auth/ +│ │ │ ├── config/ # 配置类 +| | | │ ├── RedisConfig.java # Redis配置 +│ │ │ │ └── SecurityConfig.java # 安全配置 +│ │ │ ├── controller/ # 控制器 +│ │ │ │ └── AuthController.java # 认证控制器 +│ │ │ ├── dto/ # 数据传输对象 +│ │ │ │ ├── LoginDTO.java # 登录请求DTO +│ │ │ │ ├── RegisterDTO.java # 注册请求DTO +│ │ │ │ └── TokenInfoDTO.java # 令牌信息DTO +│ │ │ ├── entity/ # 实体类 +│ │ │ │ ├── AuthUser.java # 用户实体 +│ │ │ │ ├── AuthRole.java # 角色实体 +│ │ │ │ ├── AuthPermission.java # 权限实体 +│ │ │ │ ├── AuthUserRole.java # 用户角色关联实体 +│ │ │ │ └── AuthRolePermission.java # 角色权限关联实体 +│ │ │ ├── filter/ # 过滤器 +│ │ │ │ └── JwtAuthenticationFilter.java # JWT认证过滤器 +│ │ │ ├── mapper/ # MyBatis映射接口 +│ │ │ │ ├── AuthUserMapper.java # 用户Mapper +│ │ │ │ ├── AuthRoleMapper.java # 角色Mapper +│ │ │ │ ├── AuthPermissionMapper.java # 权限Mapper +│ │ │ │ ├── AuthUserRoleMapper.java # 用户角色Mapper +│ │ │ │ └── AuthRolePermissionMapper.java # 角色权限Mapper +│ │ │ ├── service/ # 服务层 +│ │ │ │ ├── AuthUserService.java # 用户服务接口 +│ │ │ │ ├── CaptchaService.java # 验证码服务接口 +│ │ │ │ └── impl/ # 实现类 +│ │ │ │ ├── AuthUserServiceImpl.java +│ │ │ │ └── CaptchaServiceImpl.java +│ │ │ ├── utils/ # 工具类 +│ │ │ │ ├── RedisUtils.java # Redis工具类 +│ │ │ │ └── JwtUtils.java # JWT工具类 +│ │ │ └── AuthApplication.java # 启动类 +│ │ └── resources/ +│ │ ├── mapper/ # MyBatis XML映射文件 +│ │ │ └── AuthUserMapper.xml +│ │ ├── application.yml # 配置文件 +│ │ ├── application-dev.yml # 开发环境配置 +└── pom.xml +``` + +## 二、功能概述 + +### 1. 核心功能 + +- **用户认证**:登录、注册、令牌刷新、登出 +- **安全管理**:JWT生成与验证、密码加密 +- **角色权限**:用户角色和权限的管理 +- **验证码服务**:生成和验证图形验证码 + +### 2. 业务流程 + +1. **用户注册**:用户提交注册信息 → 验证码验证 → 创建用户 → 分配默认角色 +2. **用户登录**:用户提交登录信息 → 验证用户凭证 → 生成JWT令牌 → 缓存用户认证信息 +3. **认证鉴权**:请求携带JWT令牌 → JWT过滤器验证令牌 → 提取用户信息 → 设置安全上下文 → 授权访问 + +## 三、接口文档 + +### 1. 用户登录 + +- **URI**: `/auth/login` +- **方法**: POST +- **描述**: 用户登录并获取令牌 +- **请求Body**: + ```json + { + "username": "admin", + "password": "admin123", + "captcha": "1a2b", + "captchaKey": "abcd1234" + } + ``` +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiJ9...", + "refreshToken": "nQs6NGPzAJbfw5ddsSVGY2iqAomOmGW9", + "tokenType": "Bearer", + "expiresIn": 86400, + "userId": 1, + "username": "admin" + } + } + ``` +- **错误码**: + - `400`: 参数错误(如验证码错误) + - `401`: 用户名或密码错误 + - `403`: 账号已被禁用 + +### 2. 用户注册 + +- **URI**: `/auth/register` +- **方法**: POST +- **描述**: 注册新用户 +- **请求Body**: + ```json + { + "username": "newuser", + "password": "password123", + "confirmPassword": "password123", + "email": "user@example.com", + "phone": "13800138000", + "captcha": "1a2b", + "captchaKey": "abcd1234" + } + ``` +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": "注册成功" + } + ``` +- **错误码**: + - `400`: 参数错误(如密码不匹配、验证码错误) + - `1002`: 用户名已存在 + +### 3. 获取验证码 + +- **URI**: `/auth/captcha` +- **方法**: GET +- **描述**: 生成图形验证码 +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": { + "captchaKey": "abcd1234", + "captchaImage": "data:image/png;base64,iVBORw0KGgoAAAANSUhEU..." + } + } + ``` + +### 4. 刷新令牌 + +- **URI**: `/auth/refresh` +- **方法**: POST +- **描述**: 使用刷新令牌获取新的访问令牌 +- **请求参数**: + - `refreshToken`: 刷新令牌字符串 +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": { + "accessToken": "eyJhbGciOiJIUzI1NiJ9...", + "refreshToken": "new_refresh_token", + "tokenType": "Bearer", + "expiresIn": 86400, + "userId": 1, + "username": "admin" + } + } + ``` +- **错误码**: + - `401`: 刷新令牌无效或已过期 + +### 5. 用户登出 + +- **URI**: `/auth/logout` +- **方法**: POST +- **描述**: 用户登出,清除令牌 +- **请求头**: + - `Authorization`: Bearer {accessToken} +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": null + } + ``` + +### 6. 获取当前用户信息 + +- **URI**: `/auth/current` +- **方法**: GET +- **描述**: 获取当前登录用户的详细信息 +- **请求头**: + - `Authorization`: Bearer {accessToken} +- **响应**: + ```json + { + "code": 200, + "message": "操作成功", + "data": { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "phone": "13800138000", + "status": 1, + "lastLoginTime": "2024-03-30T12:30:45", + "lastLoginIp": "192.168.1.1", + "roles": ["USER_ADMIN"] + } + } + ``` +- **错误码**: + - `401`: 未授权 + - `404`: 用户不存在 + +## 四、数据库设计 + +认证模块涉及的主要数据库表: + +### 1. auth_user (用户表) +- `id`: 用户ID (主键) +- `username`: 用户名 +- `password`: 加密密码 +- `salt`: 加密盐值 +- `email`: 邮箱 +- `phone`: 手机号 +- `status`: 状态(0:禁用,1:正常) +- `last_login_time`: 最后登录时间 +- `last_login_ip`: 最后登录IP + +### 2. auth_role (角色表) +- `id`: 角色ID (主键) +- `name`: 角色名称 +- `code`: 角色编码 +- `description`: 角色描述 + +### 3. auth_permission (权限表) +- `id`: 权限ID (主键) +- `name`: 权限名称 +- `code`: 权限编码 +- `description`: 权限描述 + +### 4. auth_user_role (用户角色关联表) +- `id`: 主键 +- `user_id`: 用户ID (外键) +- `role_id`: 角色ID (外键) + +### 5. auth_role_permission (角色权限关联表) +- `id`: 主键 +- `role_id`: 角色ID (外键) +- `permission_id`: 权限ID (外键) + +## 五、认证流程说明 + +1. **JWT认证流程**: + - 用户登录成功后,服务端生成JWT令牌和刷新令牌 + - 客户端请求时在请求头中携带JWT令牌 + - 服务端通过JwtAuthenticationFilter验证令牌 + - 验证通过后,设置SecurityContext并放行请求 + +2. **权限校验流程**: + - 用户登录时加载其角色和权限 + - 将权限信息缓存到Redis中 + - 请求鉴权时从SecurityContext获取用户信息 + - 根据用户角色和权限决定是否允许访问 + +video-auth模块提供了完整的用户认证、授权和安全管理功能,为整个视频平台提供统一的身份验证服务。 \ No newline at end of file diff --git a/video-auth/pom.xml b/video-auth/pom.xml index 55a08f4..baf9e7e 100644 --- a/video-auth/pom.xml +++ b/video-auth/pom.xml @@ -12,12 +12,19 @@ video-auth + com.nuaa.video video-common ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + org.springframework.boot @@ -28,7 +35,100 @@ org.springframework.security spring-security-oauth2-authorization-server - 1.2.1 + 1.2.3 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.11 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.projectlombok + lombok + true + + + + + cn.hutool + hutool-all + 5.8.26 + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/AuthApplication.java b/video-auth/src/main/java/com/nuaa/video/auth/AuthApplication.java index 7ba42cd..1f7db2b 100644 --- a/video-auth/src/main/java/com/nuaa/video/auth/AuthApplication.java +++ b/video-auth/src/main/java/com/nuaa/video/auth/AuthApplication.java @@ -1,11 +1,14 @@ 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); diff --git a/video-auth/src/main/java/com/nuaa/video/auth/config/RedisConfig.java b/video-auth/src/main/java/com/nuaa/video/auth/config/RedisConfig.java new file mode 100644 index 0000000..3d9f812 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/config/RedisConfig.java @@ -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 redisTemplate(RedisConnectionFactory factory) { + RedisTemplate 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 serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class); + + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(serializer); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + + template.afterPropertiesSet(); + return template; + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/config/SecurityConfig.java b/video-auth/src/main/java/com/nuaa/video/auth/config/SecurityConfig.java new file mode 100644 index 0000000..e6b213d --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/config/SecurityConfig.java @@ -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(); + } + + +} diff --git a/video-auth/src/main/java/com/nuaa/video/auth/controller/AuthController.java b/video-auth/src/main/java/com/nuaa/video/auth/controller/AuthController.java new file mode 100644 index 0000000..01998eb --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/controller/AuthController.java @@ -0,0 +1,116 @@ +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 login(@RequestBody @Valid LoginDTO loginDTO) { + return authUserService.login(loginDTO); + } + + /** + * 用户注册 + */ + @PostMapping("/register") + public Result register(@RequestBody @Valid RegisterDTO registerDTO) { + return authUserService.register(registerDTO); + } + + /** + * 生成验证码 + */ + @GetMapping("/captcha") + public Result> captcha() { + return captchaService.generateCaptcha(); + } + + /** + * 刷新令牌 + */ + @PostMapping("/refresh") + public Result refreshToken(@RequestParam("refreshToken") String refreshToken) { + return authUserService.refreshToken(refreshToken); + } + + /** + * 用户登出 + */ + @PostMapping("/logout") + public Result logout(HttpServletRequest request) { + Long userId = getUserIdFromRequest(request); + if (userId == null) { + return Result.error("未获取到用户ID"); + } + return authUserService.logout(userId); + } + + /** + * 获取当前用户信息 + */ + @GetMapping("/current") + public Result 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; + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/dto/LoginDTO.java b/video-auth/src/main/java/com/nuaa/video/auth/dto/LoginDTO.java new file mode 100644 index 0000000..6d8c13e --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/dto/LoginDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/dto/RegisterDTO.java b/video-auth/src/main/java/com/nuaa/video/auth/dto/RegisterDTO.java new file mode 100644 index 0000000..16c54e5 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/dto/RegisterDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/dto/TokenInfoDTO.java b/video-auth/src/main/java/com/nuaa/video/auth/dto/TokenInfoDTO.java new file mode 100644 index 0000000..6973003 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/dto/TokenInfoDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthPermission.java b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthPermission.java new file mode 100644 index 0000000..6d0cd4e --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthPermission.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRole.java b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRole.java new file mode 100644 index 0000000..66981c8 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRole.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRolePermission.java b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRolePermission.java new file mode 100644 index 0000000..07bb5cd --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRolePermission.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUser.java b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUser.java new file mode 100644 index 0000000..b6c2f68 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUser.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUserRole.java b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUserRole.java new file mode 100644 index 0000000..ebd2831 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUserRole.java @@ -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; +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/filter/JwtAuthenticationFilter.java b/video-auth/src/main/java/com/nuaa/video/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..e5f7edc --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/filter/JwtAuthenticationFilter.java @@ -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 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; + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthPermissionMapper.java b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthPermissionMapper.java new file mode 100644 index 0000000..4f1b8c6 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthPermissionMapper.java @@ -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 { +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRoleMapper.java b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRoleMapper.java new file mode 100644 index 0000000..16123ad --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRoleMapper.java @@ -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 { +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRolePermissionMapper.java b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRolePermissionMapper.java new file mode 100644 index 0000000..3a034a2 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRolePermissionMapper.java @@ -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 { +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserMapper.java b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserMapper.java new file mode 100644 index 0000000..f1f3e43 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserMapper.java @@ -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 { + + /** + * 获取用户角色 + * + * @param userId 用户ID + * @return 角色编码列表 + */ + List getUserRoles(@Param("userId") Long userId); + + /** + * 获取用户权限 + * + * @param userId 用户ID + * @return 权限编码列表 + */ + List getUserPermissions(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserRoleMapper.java b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserRoleMapper.java new file mode 100644 index 0000000..1d39c44 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserRoleMapper.java @@ -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 { +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java b/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java new file mode 100644 index 0000000..84d0f7f --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java @@ -0,0 +1,62 @@ +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 { + + /** + * 用户登录 + * + * @param loginDTO 登录信息 + * @return 登录结果 + */ + Result login(LoginDTO loginDTO); + + /** + * 用户注册 + * + * @param registerDTO 注册信息 + * @return 注册结果 + */ + Result register(RegisterDTO registerDTO); + + /** + * 通过用户名查询用户 + * + * @param username 用户名 + * @return 用户信息 + */ + AuthUser getByUsername(String username); + + /** + * 刷新令牌 + * + * @param refreshToken 刷新令牌 + * @return 新的令牌信息 + */ + Result refreshToken(String refreshToken); + + /** + * 登出 + * + * @param userId 用户ID + * @return 登出结果 + */ + Result logout(Long userId); + + /** + * 获取当前登录用户信息 + * + * @param userId 用户ID + * @return 用户信息 + */ + Result getCurrentUser(Long userId); +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/CaptchaService.java b/video-auth/src/main/java/com/nuaa/video/auth/service/CaptchaService.java new file mode 100644 index 0000000..11377d4 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/CaptchaService.java @@ -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> generateCaptcha(); +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java new file mode 100644 index 0000000..dbcb8f3 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java @@ -0,0 +1,324 @@ +package com.nuaa.video.auth.service.impl; + +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.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.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户认证服务实现类 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthUserServiceImpl extends ServiceImpl implements AuthUserService { + + private final AuthUserMapper authUserMapper; + private final AuthUserRoleMapper authUserRoleMapper; + private final PasswordEncoder passwordEncoder; + private final JwtUtils jwtUtils; + private final RedisUtils redisUtils; + + @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 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 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().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().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); + + return Result.success("注册成功"); + } + + @Override + public AuthUser getByUsername(String username) { + return getOne(new LambdaQueryWrapper().eq(AuthUser::getUsername, username)); + } + + @Override + public Result 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 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 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 roles = getUserRoles(userId); + user.setRoles(roles.toArray(new String[0])); + + return Result.success(user); + } + + /** + * 获取用户角色 + * + * @param userId 用户ID + * @return 角色列表 + */ + private List getUserRoles(Long userId) { + // 优先从缓存获取 + @SuppressWarnings("unchecked") + List roles = (List) 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 getUserPermissions(Long userId) { + // 优先从缓存获取 + @SuppressWarnings("unchecked") + List permissions = (List) 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 roles = authUserMapper.getUserRoles(userId); + if (!roles.isEmpty()) { + redisUtils.set(USER_ROLE_PREFIX + userId, roles, expiration); + } + + // 缓存用户权限 + List permissions = authUserMapper.getUserPermissions(userId); + if (!permissions.isEmpty()) { + redisUtils.set(USER_PERMISSION_PREFIX + userId, permissions, expiration); + } + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/impl/CaptchaServiceImpl.java b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/CaptchaServiceImpl.java new file mode 100644 index 0000000..d2ac90c --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/CaptchaServiceImpl.java @@ -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> 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 result = new HashMap<>(); + result.put("captchaKey", captchaKey); + result.put("captchaImage", captchaBase64); + + return Result.success(result); + } catch (Exception e) { + log.error("生成验证码失败", e); + return Result.error("生成验证码失败"); + } + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/utils/JwtUtils.java b/video-auth/src/main/java/com/nuaa/video/auth/utils/JwtUtils.java new file mode 100644 index 0000000..e83b2f5 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/utils/JwtUtils.java @@ -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 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 getClaimFromToken(String token, Function 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 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); + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/utils/RedisUtils.java b/video-auth/src/main/java/com/nuaa/video/auth/utils/RedisUtils.java new file mode 100644 index 0000000..e091e91 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/utils/RedisUtils.java @@ -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 redisTemplate; + + public RedisUtils(RedisTemplate 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) 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; + } + } + + // 其他必要的方法可以根据需要添加 +} \ No newline at end of file diff --git a/video-auth/src/main/resources/application-dev.yml b/video-auth/src/main/resources/application-dev.yml new file mode 100644 index 0000000..35589b3 --- /dev/null +++ b/video-auth/src/main/resources/application-dev.yml @@ -0,0 +1,26 @@ +spring: + datasource: + dynamic: + primary: master + strict: false + datasource: + master: + url: jdbc:mysql://5.180.253.115:3306/video_auth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: sty@G458221 + driver-class-name: com.mysql.cj.jdbc.Driver + + data: + redis: + database: 0 + host: 5.180.253.115 + port: 6379 + password: redis_EsJ8FX + timeout: 10000 + +logging: + level: + com.nuaa.video: debug + org.springframework.security: debug + org.springframework.beans: debug + org.springframework.context: debug \ No newline at end of file diff --git a/video-auth/src/main/resources/application.yml b/video-auth/src/main/resources/application.yml index 1255da4..1f7f282 100644 --- a/video-auth/src/main/resources/application.yml +++ b/video-auth/src/main/resources/application.yml @@ -4,35 +4,87 @@ server: spring: application: name: video-auth + profiles: + active: dev + config: + import: + - optional:nacos:${spring.application.name}.yaml cloud: nacos: discovery: - server-addr: 5.180.253.115:8848 - namespace: public - group: DEFAULT_GROUP + server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848} + namespace: ${NACOS_NAMESPACE:public} + group: ${NACOS_GROUP:DEFAULT_GROUP} + config: + server-addr: ${spring.cloud.nacos.discovery.server-addr} + namespace: ${spring.cloud.nacos.discovery.namespace} + group: ${spring.cloud.nacos.discovery.group} + file-extension: yaml sentinel: transport: - dashboard: 5.180.253.115:8080 + dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080} port: 8720 eager: true + datasource: - url: jdbc:mysql://5.180.253.115:3306/video_auth?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai - username: root - password: sty@G458221 driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${MYSQL_HOST:5.180.253.115}:${MYSQL_PORT:3306}/video_auth?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: ${MYSQL_USERNAME:root} + password: ${MYSQL_PASSWORD:sty@G458221} + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + idle-timeout: 30000 + max-lifetime: 1200000 + connection-timeout: 30000 + connection-test-query: SELECT 1 + data: redis: - host: 5.180.253.115 - port: 6379 - password: redis_EsJ8FX + host: ${REDIS_HOST:5.180.253.115} + port: ${REDIS_PORT:6379} + password: ${REDIS_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: video-platform-secret-key-2024-nuaa expiration: 86400 - token-header: Authorization \ No newline at end of file + token-header: Authorization + token-prefix: "Bearer " + +management: + endpoints: + web: + exposure: + include: '*' + endpoint: + health: + show-details: always + +logging: + level: + com.nuaa.video: info + org.springframework.security: info + org.springframework.beans: debug + org.springframework.context: debug \ No newline at end of file diff --git a/video-auth/src/main/resources/mapper/AuthPermissionMapper.xml b/video-auth/src/main/resources/mapper/AuthPermissionMapper.xml new file mode 100644 index 0000000..46a4293 --- /dev/null +++ b/video-auth/src/main/resources/mapper/AuthPermissionMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/video-auth/src/main/resources/mapper/AuthRoleMapper.xml b/video-auth/src/main/resources/mapper/AuthRoleMapper.xml new file mode 100644 index 0000000..b063b49 --- /dev/null +++ b/video-auth/src/main/resources/mapper/AuthRoleMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/video-auth/src/main/resources/mapper/AuthRolePermissionMapper.xml b/video-auth/src/main/resources/mapper/AuthRolePermissionMapper.xml new file mode 100644 index 0000000..64bb061 --- /dev/null +++ b/video-auth/src/main/resources/mapper/AuthRolePermissionMapper.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/video-auth/src/main/resources/mapper/AuthUserMapper.xml b/video-auth/src/main/resources/mapper/AuthUserMapper.xml new file mode 100644 index 0000000..066fd0b --- /dev/null +++ b/video-auth/src/main/resources/mapper/AuthUserMapper.xml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java index 1e1653b..c994d23 100644 --- a/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java +++ b/video-common/src/main/java/com/nuaa/video/common/config/MybatisPlusConfig.java @@ -1,4 +1,3 @@ -// com.nuaa.video.common.config.MybatisPlusConfig.java package com.nuaa.video.common.config; import com.baomidou.mybatisplus.annotation.DbType;