From 8c4d8e4c6a724cb2f22fa8dfa583e996f7c5db8d Mon Sep 17 00:00:00 2001 From: Toyas Date: Mon, 31 Mar 2025 02:21:04 +0800 Subject: [PATCH] feat(auth): Implement authentication module foundation --- .idea/.cache/.Apifox_Helper/.toolWindow.db | Bin 0 -> 122880 bytes .idea/ApifoxUploaderProjectSetting.xml | 7 + .idea/fastRequest/fastRequestCollection.xml | 23 ++ .../fastRequestCurrentProjectLocalConfig.xml | 3 + .../video-auth模块目录架构、功能及接口文档.md | 269 +++++++++++++++ video-auth/pom.xml | 102 +++++- .../com/nuaa/video/auth/AuthApplication.java | 3 + .../nuaa/video/auth/config/RedisConfig.java | 38 ++ .../video/auth/config/SecurityConfig.java | 85 +++++ .../video/auth/controller/AuthController.java | 116 +++++++ .../com/nuaa/video/auth/dto/LoginDTO.java | 36 ++ .../com/nuaa/video/auth/dto/RegisterDTO.java | 60 ++++ .../com/nuaa/video/auth/dto/TokenInfoDTO.java | 45 +++ .../video/auth/entity/AuthPermission.java | 59 ++++ .../com/nuaa/video/auth/entity/AuthRole.java | 59 ++++ .../video/auth/entity/AuthRolePermission.java | 44 +++ .../com/nuaa/video/auth/entity/AuthUser.java | 91 +++++ .../nuaa/video/auth/entity/AuthUserRole.java | 44 +++ .../auth/filter/JwtAuthenticationFilter.java | 84 +++++ .../auth/mapper/AuthPermissionMapper.java | 12 + .../video/auth/mapper/AuthRoleMapper.java | 12 + .../auth/mapper/AuthRolePermissionMapper.java | 12 + .../video/auth/mapper/AuthUserMapper.java | 31 ++ .../video/auth/mapper/AuthUserRoleMapper.java | 12 + .../video/auth/service/AuthUserService.java | 62 ++++ .../video/auth/service/CaptchaService.java | 18 + .../service/impl/AuthUserServiceImpl.java | 324 ++++++++++++++++++ .../auth/service/impl/CaptchaServiceImpl.java | 65 ++++ .../com/nuaa/video/auth/utils/JwtUtils.java | 124 +++++++ .../com/nuaa/video/auth/utils/RedisUtils.java | 105 ++++++ .../src/main/resources/application-dev.yml | 26 ++ video-auth/src/main/resources/application.yml | 74 +++- .../resources/mapper/AuthPermissionMapper.xml | 4 + .../main/resources/mapper/AuthRoleMapper.xml | 4 + .../mapper/AuthRolePermissionMapper.xml | 4 + .../main/resources/mapper/AuthUserMapper.xml | 27 ++ .../common/config/MybatisPlusConfig.java | 1 - 37 files changed, 2072 insertions(+), 13 deletions(-) create mode 100644 .idea/.cache/.Apifox_Helper/.toolWindow.db create mode 100644 .idea/fastRequest/fastRequestCollection.xml create mode 100644 docs/video-auth模块目录架构、功能及接口文档.md create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/config/RedisConfig.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/config/SecurityConfig.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/controller/AuthController.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/dto/LoginDTO.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/dto/RegisterDTO.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/dto/TokenInfoDTO.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/entity/AuthPermission.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRole.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/entity/AuthRolePermission.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUser.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/entity/AuthUserRole.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/filter/JwtAuthenticationFilter.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthPermissionMapper.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRoleMapper.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthRolePermissionMapper.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserMapper.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/mapper/AuthUserRoleMapper.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/service/CaptchaService.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/service/impl/CaptchaServiceImpl.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/utils/JwtUtils.java create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/utils/RedisUtils.java create mode 100644 video-auth/src/main/resources/application-dev.yml create mode 100644 video-auth/src/main/resources/mapper/AuthPermissionMapper.xml create mode 100644 video-auth/src/main/resources/mapper/AuthRoleMapper.xml create mode 100644 video-auth/src/main/resources/mapper/AuthRolePermissionMapper.xml create mode 100644 video-auth/src/main/resources/mapper/AuthUserMapper.xml diff --git a/.idea/.cache/.Apifox_Helper/.toolWindow.db b/.idea/.cache/.Apifox_Helper/.toolWindow.db new file mode 100644 index 0000000000000000000000000000000000000000..a5693ff0141c0e3b0be46fbbf61a7d88e6f199b8 GIT binary patch literal 122880 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 + \ 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;