feat(auth): Implement authentication module foundation
This commit is contained in:
BIN
Binary file not shown.
Generated
+7
@@ -2,5 +2,12 @@
|
||||
<project version="4">
|
||||
<component name="ApifoxUploaderProjectSetting">
|
||||
<option name="apiAccessToken" value="APS-8W5ece3ancukaCFg7XU25oGRku4hR3r5" />
|
||||
<option name="apiProjectIds">
|
||||
<array>
|
||||
<option value="<byte-array>rO0ABXNyADZjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50LlByb2plY3RBbmRNb2R1bGUAAAAAAAAAAQIAFVoABmVuYWJsZUwACG1vZHVsZUlkdAASTGphdmEvbGFuZy9TdHJpbmc7TAAGb3RoZXIxcQB+AAFMAAdvdGhlcjEwcQB+AAFMAAdvdGhlcjExcQB+AAFMAAdvdGhlcjEycQB+AAFMAAZvdGhlcjJxAH4AAUwABm90aGVyM3EAfgABTAAGb3RoZXI0cQB+AAFMAAZvdGhlcjVxAH4AAUwABm90aGVyNnEAfgABTAAGb3RoZXI3cQB+AAFMAAZvdGhlcjhxAH4AAUwABm90aGVyOXEAfgABTAAKcGF0aEJlZm9yZXEAfgABTAANcHJvamVjdEZvbGRlcnEAfgABTAAPcHJvamVjdEZvbGRlcklkcQB+AAFMAAlwcm9qZWN0SWRxAH4AAUwAC3Byb2plY3ROYW1lcQB+AAFMAAxzY2hlbWFGb2xkZXJxAH4AAUwACHNjaGVtYUlkcQB+AAF4cAF0AAp2aWRlby1hdXRocHBwcHBwcHBwcHBwdAAAdAAKdmlkZW8tYXV0aHQACzUuMzQ1NTAwMUU3dAAJNjEyNjYzNC4wdAAGSEEtT1ZQdAAJ5qC555uu5b2VdAABMA==</byte-array>" />
|
||||
</array>
|
||||
</option>
|
||||
<option name="treeNodes" value="<byte-array>rO0ABXNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAXQABzI2NTY1MjFzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgALTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAIZnVsbFBhdGhxAH4ABUwAA2tleXEAfgAFTAAEbmFtZXEAfgAFTAAIcGFyZW50SWRxAH4ABUwACXByb2plY3RJZHEAfgAFTAALcHJvamVjdE5hbWVxAH4ABUwABnRlYW1JZHEAfgAFTAAIdGVhbU5hbWVxAH4ABUwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQADOS4quS6uuWboumYn3NxAH4AAD9AAAAAAAAMdwgAAAAQAAAABHQABzQ2NTczMjZzcQB+AAR0ABnkuKrkurrlm6LpmJ8v5Liq5Lq66aG555uuc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcQB+AAt0ABbkuKrkurrpobnnm64gKDQ2NTczMjYpdAAHMjY1NjUyMXQABzQ2NTczMjZ0AAzkuKrkurrpobnnm650AAcyNjU2NTIxcH5yAC5jb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50Lk5vZGVUeXBlAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdAAHUFJPSkVDVHQABzQ2ODIxMTBzcQB+AAR0ABbkuKrkurrlm6LpmJ8veXF4cy1zZWxmc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcQB+ABh0ABN5cXhzLXNlbGYgKDQ2ODIxMTApdAAHMjY1NjUyMXQABzQ2ODIxMTB0AAl5cXhzLXNlbGZ0AAcyNjU2NTIxcHEAfgAWdAAHNDc1NzQ0NHNxAH4ABHQAFuS4quS6uuWboumYny95cXhzLXN5bmNzcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4AHBxAH4AIXQAE3lxeHMtc3luYyAoNDc1NzQ0NCl0AAcyNjU2NTIxdAAHNDc1NzQ0NHQACXlxeHMtc3luY3QABzI2NTY1MjFwcQB+ABZ0AAc2MTI2NjM0c3EAfgAEdAAT5Liq5Lq65Zui6ZifL0hBLU9WUHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAXQACzUuMzQ1NTAwMUU3c3EAfgAEdAAe5Liq5Lq65Zui6ZifL0hBLU9WUC92aWRlby1hdXRoc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeAB0AAp2aWRlby1hdXRocQB+AC50AAp2aWRlby1hdXRodAAHNjEyNjYzNHQACTYxMjY2MzQuMHQABkhBLU9WUHBwfnEAfgAUdAAGRk9MREVSeABwcQB+ACp0ABBIQS1PVlAgKDYxMjY2MzQpdAAHMjY1NjUyMXQABzYxMjY2MzR0AAZIQS1PVlB0AAcyNjU2NTIxcHEAfgAWeABwcQB+AAN0AAzkuKrkurrlm6LpmJ9wcHB0AAcyNjU2NTIxdAAM5Liq5Lq65Zui6ZiffnEAfgAUdAAEVEVBTXgA</byte-array>" />
|
||||
<option name="treeNodesJTree" value="<byte-array>rO0ABXNyACFqYXZheC5zd2luZy50cmVlLkRlZmF1bHRUcmVlTW9kZWynvpEmGsXl2QMAA1oAEmFza3NBbGxvd3NDaGlsZHJlbkwADGxpc3RlbmVyTGlzdHQAJUxqYXZheC9zd2luZy9ldmVudC9FdmVudExpc3RlbmVyTGlzdDtMAARyb290dAAbTGphdmF4L3N3aW5nL3RyZWUvVHJlZU5vZGU7eHAAc3IAI2phdmF4LnN3aW5nLmV2ZW50LkV2ZW50TGlzdGVuZXJMaXN0kUjMLXPfDt4DAAB4cHB4c3IAJ2phdmF4LnN3aW5nLnRyZWUuRGVmYXVsdE11dGFibGVUcmVlTm9kZcRYv/zyqHHgAwADWgAOYWxsb3dzQ2hpbGRyZW5MAAhjaGlsZHJlbnQAEkxqYXZhL3V0aWwvVmVjdG9yO0wABnBhcmVudHQAIkxqYXZheC9zd2luZy90cmVlL011dGFibGVUcmVlTm9kZTt4cAFzcgAQamF2YS51dGlsLlZlY3RvctmXfVuAO68BAwADSQARY2FwYWNpdHlJbmNyZW1lbnRJAAxlbGVtZW50Q291bnRbAAtlbGVtZW50RGF0YXQAE1tMamF2YS9sYW5nL09iamVjdDt4cAAAAAAAAAABdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAACnNxAH4ABgFzcQB+AAoAAAAAAAAABHVxAH4ADQAAAApzcQB+AAYBcHEAfgAPdXEAfgANAAAAAnQACnVzZXJPYmplY3RzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgALTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAIZnVsbFBhdGhxAH4AFkwAA2tleXEAfgAWTAAEbmFtZXEAfgAWTAAIcGFyZW50SWRxAH4AFkwACXByb2plY3RJZHEAfgAWTAALcHJvamVjdE5hbWVxAH4AFkwABnRlYW1JZHEAfgAWTAAIdGVhbU5hbWVxAH4AFkwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQAGeS4quS6uuWboumYny/kuKrkurrpobnnm65zcgAXamF2YS51dGlsLkxpbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4AHB0AAc0NjU3MzI2dAAW5Liq5Lq66aG555uuICg0NjU3MzI2KXQABzI2NTY1MjF0AAc0NjU3MzI2dAAM5Liq5Lq66aG555uudAAHMjY1NjUyMXB+cgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5Ob2RlVHlwZQAAAAAAAAAAEgAAeHIADmphdmEubGFuZy5FbnVtAAAAAAAAAAASAAB4cHQAB1BST0pFQ1R4c3EAfgAGAXBxAH4AD3VxAH4ADQAAAAJxAH4AFHNxAH4AFXQAFuS4quS6uuWboumYny95cXhzLXNlbGZzcQB+ABs/QAAAAAAAAHcIAAAAEAAAAAB4AHB0AAc0NjgyMTEwdAATeXF4cy1zZWxmICg0NjgyMTEwKXQABzI2NTY1MjF0AAc0NjgyMTEwdAAJeXF4cy1zZWxmdAAHMjY1NjUyMXBxAH4AJnhzcQB+AAYBcHEAfgAPdXEAfgANAAAAAnEAfgAUc3EAfgAVdAAW5Liq5Lq65Zui6ZifL3lxeHMtc3luY3NxAH4AGz9AAAAAAAAAdwgAAAAQAAAAAHgAcHQABzQ3NTc0NDR0ABN5cXhzLXN5bmMgKDQ3NTc0NDQpdAAHMjY1NjUyMXQABzQ3NTc0NDR0AAl5cXhzLXN5bmN0AAcyNjU2NTIxcHEAfgAmeHNxAH4ABgFzcQB+AAoAAAAAAAAAAXVxAH4ADQAAAApzcQB+AAYBcHEAfgA+dXEAfgANAAAAAnEAfgAUc3EAfgAVdAAe5Liq5Lq65Zui6ZifL0hBLU9WUC92aWRlby1hdXRoc3EAfgAbP0AAAAAAAAB3CAAAABAAAAAAeAB0AAp2aWRlby1hdXRodAALNS4zNDU1MDAxRTd0AAp2aWRlby1hdXRodAAHNjEyNjYzNHQACTYxMjY2MzQuMHQABkhBLU9WUHBwfnEAfgAkdAAGRk9MREVSeHBwcHBwcHBwcHhxAH4AD3VxAH4ADQAAAAJxAH4AFHNxAH4AFXQAE+S4quS6uuWboumYny9IQS1PVlBzcQB+ABs/QAAAAAAAAHcIAAAAEAAAAAB4AHB0AAc2MTI2NjM0dAAQSEEtT1ZQICg2MTI2NjM0KXQABzI2NTY1MjF0AAc2MTI2NjM0dAAGSEEtT1ZQdAAHMjY1NjUyMXBxAH4AJnhwcHBwcHB4cQB+AAl1cQB+AA0AAAACcQB+ABRzcQB+ABV0AAzkuKrkurrlm6LpmJ9zcQB+ABs/QAAAAAAAAHcIAAAAEAAAAAB4AHB0AAcyNjU2NTIxdAAM5Liq5Lq65Zui6ZifcHBwdAAHMjY1NjUyMXQADOS4quS6uuWboumYn35xAH4AJHQABFRFQU14cHBwcHBwcHBweHB1cQB+AA0AAAACcQB+ABRzcQB+ABV0AARSb290cHB0AAEwcQB+AGRwcHBwcHEAfgBgeHNxAH4ACgAAAAAAAAACdXEAfgANAAAACnQABHJvb3RxAH4ACXBwcHBwcHBweHg=</byte-array>" />
|
||||
</component>
|
||||
</project>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="fastRequestCollection">
|
||||
<option name="detail">
|
||||
<CollectionDetail>
|
||||
<option name="childList">
|
||||
<list>
|
||||
<CollectionDetail>
|
||||
<option name="groupId" value="1" />
|
||||
<option name="id" value="1" />
|
||||
<option name="name" value="Default Group" />
|
||||
<option name="type" value="1" />
|
||||
</CollectionDetail>
|
||||
</list>
|
||||
</option>
|
||||
<option name="groupId" value="-1" />
|
||||
<option name="id" value="0" />
|
||||
<option name="name" value="Root" />
|
||||
<option name="type" value="1" />
|
||||
</CollectionDetail>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="fastRequestCurrentProjectLocalConfig">
|
||||
<option name="domain" value="localhost:8082" />
|
||||
<option name="enableEnv" value="yqxs" />
|
||||
<option name="enableProject" value="yqxs" />
|
||||
<option name="initFlag" value="1" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -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模块提供了完整的用户认证、授权和安全管理功能,为整个视频平台提供统一的身份验证服务。
|
||||
+101
-1
@@ -12,12 +12,19 @@
|
||||
<artifactId>video-auth</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<!-- 引入公共模块 -->
|
||||
<dependency>
|
||||
<groupId>com.nuaa.video</groupId>
|
||||
<artifactId>video-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security OAuth2 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -28,7 +35,100 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-authorization-server</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<version>1.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Alibaba Nacos Discovery -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Alibaba Nacos Config -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Actuator -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud OpenFeign -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Cloud Alibaba Sentinel -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Connector -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>3.5.11</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Hutool工具包 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.26</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.nuaa.video.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
// 创建配置好的ObjectMapper
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer
|
||||
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(mapper, Object.class);
|
||||
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.nuaa.video.auth.config;
|
||||
|
||||
import cn.hutool.jwt.Claims;
|
||||
import com.nuaa.video.auth.filter.JwtAuthenticationFilter;
|
||||
import com.nuaa.video.auth.service.AuthUserService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.servlet.View;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 安全配置
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
private final AuthUserService authUserService;
|
||||
|
||||
// 使用构造注入并添加@Lazy注解
|
||||
public SecurityConfig(@Lazy AuthUserService authUserService) {
|
||||
this.authUserService = authUserService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, View error, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.cors(cors -> cors.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
"/auth/login",
|
||||
"/auth/register",
|
||||
"/auth/captcha",
|
||||
"/actuator/**",
|
||||
"/error"
|
||||
).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
//添加JWT认证过滤器
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,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<TokenInfoDTO> login(@RequestBody @Valid LoginDTO loginDTO) {
|
||||
return authUserService.login(loginDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public Result<String> register(@RequestBody @Valid RegisterDTO registerDTO) {
|
||||
return authUserService.register(registerDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
public Result<Map<String, Object>> captcha() {
|
||||
return captchaService.generateCaptcha();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@PostMapping("/refresh")
|
||||
public Result<TokenInfoDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
|
||||
return authUserService.refreshToken(refreshToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未获取到用户ID");
|
||||
}
|
||||
return authUserService.logout(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/current")
|
||||
public Result<AuthUser> currentUser(HttpServletRequest request) {
|
||||
Long userId = getUserIdFromRequest(request);
|
||||
if (userId == null) {
|
||||
return Result.error("未获取到用户ID");
|
||||
}
|
||||
return authUserService.getCurrentUser(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取用户ID
|
||||
*/
|
||||
private Long getUserIdFromRequest(HttpServletRequest request) {
|
||||
// 首先尝试从请求属性中获取
|
||||
Object userIdAttr = request.getAttribute("userId");
|
||||
if (userIdAttr != null) {
|
||||
return (Long) userIdAttr;
|
||||
}
|
||||
|
||||
// 如果请求属性中没有,尝试从认证上下文获取
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getDetails() instanceof Long) {
|
||||
return (Long) authentication.getDetails();
|
||||
}
|
||||
|
||||
// 最后尝试从请求头获取(如果前端直接传递)
|
||||
String userIdHeader = request.getHeader("userId");
|
||||
if (userIdHeader != null && !userIdHeader.isEmpty()) {
|
||||
try {
|
||||
return Long.parseLong(userIdHeader);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("Invalid userId header: {}", userIdHeader);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.nuaa.video.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class LoginDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
private String captcha;
|
||||
|
||||
/**
|
||||
* 验证码KEY
|
||||
*/
|
||||
private String captchaKey;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.nuaa.video.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 注册请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class RegisterDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_]{4,16}$", message = "用户名必须为4-16位字母、数字或下划线")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Pattern(regexp = "^.{6,20}$", message = "密码长度必须为6-20位")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 确认密码
|
||||
*/
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@NotBlank(message = "验证码不能为空")
|
||||
private String captcha;
|
||||
|
||||
/**
|
||||
* 验证码KEY
|
||||
*/
|
||||
@NotBlank(message = "验证码KEY不能为空")
|
||||
private String captchaKey;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.nuaa.video.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Token信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class TokenInfoDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
private String refreshToken;
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
/**
|
||||
* 过期时间(秒)
|
||||
*/
|
||||
private long expiresIn;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.nuaa.video.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 权限实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("auth_permission")
|
||||
public class AuthPermission implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 权限ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 权限名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.nuaa.video.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("auth_role")
|
||||
public class AuthRole implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 角色编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 角色描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.nuaa.video.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色权限关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("auth_role_permission")
|
||||
public class AuthRolePermission implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 权限ID
|
||||
*/
|
||||
private Long permissionId;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.nuaa.video.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户认证实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("auth_user")
|
||||
public class AuthUser implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 加密盐值
|
||||
*/
|
||||
private String salt;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 状态(0:禁用,1:正常)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
/**
|
||||
* 最后登录IP
|
||||
*/
|
||||
private String lastLoginIp;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
private String updatedBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 拥有的角色列表(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private String[] roles;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.nuaa.video.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体
|
||||
*/
|
||||
@Data
|
||||
@TableName("auth_user_role")
|
||||
public class AuthUserRole implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
private String createdBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.nuaa.video.auth.filter;
|
||||
|
||||
import com.nuaa.video.auth.utils.JwtUtils;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtils jwtUtils;
|
||||
private static final String TOKEN_PREFIX = "Bearer ";
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
// 从请求头中获取JWT令牌
|
||||
String token = getJwtFromRequest(request);
|
||||
|
||||
// 如果令牌存在且有效,则设置认证信息
|
||||
if (StringUtils.hasText(token)) {
|
||||
try {
|
||||
// 验证令牌并提取用户ID
|
||||
Long userId = jwtUtils.getUserIdFromToken(token);
|
||||
String username = jwtUtils.getUsernameFromToken(token);
|
||||
|
||||
if (userId != null && !jwtUtils.isTokenExpired(token)) {
|
||||
log.debug("JWT验证成功, userId={}, username={}", userId, username);
|
||||
|
||||
// 设置用户权限(这里使用空列表,实际应用中可以从令牌或数据库获取)
|
||||
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
|
||||
|
||||
// 创建认证对象
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(username, null, authorities);
|
||||
|
||||
// 设置额外信息
|
||||
authentication.setDetails(userId);
|
||||
|
||||
// 设置认证信息到安全上下文
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
// 在请求属性中存储用户ID,供控制器使用
|
||||
request.setAttribute("userId", userId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败: {}", e.getMessage());
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader("Authorization");
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.nuaa.video.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.auth.entity.AuthPermission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 权限Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthPermissionMapper extends BaseMapper<AuthPermission> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.nuaa.video.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.auth.entity.AuthRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthRoleMapper extends BaseMapper<AuthRole> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.nuaa.video.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.auth.entity.AuthRolePermission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色权限关联Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthRolePermissionMapper extends BaseMapper<AuthRolePermission> {
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.nuaa.video.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.auth.entity.AuthUser;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthUserMapper extends BaseMapper<AuthUser> {
|
||||
|
||||
/**
|
||||
* 获取用户角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色编码列表
|
||||
*/
|
||||
List<String> getUserRoles(@Param("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户权限
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 权限编码列表
|
||||
*/
|
||||
List<String> getUserPermissions(@Param("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.nuaa.video.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.nuaa.video.auth.entity.AuthUserRole;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 用户角色关联Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthUserRoleMapper extends BaseMapper<AuthUserRole> {
|
||||
}
|
||||
@@ -0,0 +1,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<AuthUser> {
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param loginDTO 登录信息
|
||||
* @return 登录结果
|
||||
*/
|
||||
Result<TokenInfoDTO> login(LoginDTO loginDTO);
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerDTO 注册信息
|
||||
* @return 注册结果
|
||||
*/
|
||||
Result<String> register(RegisterDTO registerDTO);
|
||||
|
||||
/**
|
||||
* 通过用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
*/
|
||||
AuthUser getByUsername(String username);
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 新的令牌信息
|
||||
*/
|
||||
Result<TokenInfoDTO> refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 登出结果
|
||||
*/
|
||||
Result<Void> logout(Long userId);
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
Result<AuthUser> getCurrentUser(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.nuaa.video.auth.service;
|
||||
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码服务接口
|
||||
*/
|
||||
public interface CaptchaService {
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @return 验证码信息
|
||||
*/
|
||||
Result<Map<String, Object>> generateCaptcha();
|
||||
}
|
||||
@@ -0,0 +1,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<AuthUserMapper, AuthUser> 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<TokenInfoDTO> login(LoginDTO loginDTO) {
|
||||
// 验证验证码
|
||||
if (StringUtils.hasText(loginDTO.getCaptchaKey()) && StringUtils.hasText(loginDTO.getCaptcha())) {
|
||||
String captchaCache = (String) redisUtils.get(CAPTCHA_PREFIX + loginDTO.getCaptchaKey());
|
||||
if (captchaCache == null) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码已过期");
|
||||
}
|
||||
if (!loginDTO.getCaptcha().equalsIgnoreCase(captchaCache)) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码错误");
|
||||
}
|
||||
// 验证通过后移除验证码
|
||||
redisUtils.del(CAPTCHA_PREFIX + loginDTO.getCaptchaKey());
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
AuthUser user = getByUsername(loginDTO.getUsername());
|
||||
if (user == null) {
|
||||
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), "用户名或密码错误");
|
||||
}
|
||||
|
||||
// 校验密码
|
||||
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
|
||||
return Result.error(ResultCode.PASSWORD_ERROR.getCode(), "用户名或密码错误");
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() != 1) {
|
||||
return Result.error(ResultCode.FORBIDDEN.getCode(), "账号已被禁用");
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
user.setLastLoginTime(LocalDateTime.now());
|
||||
updateById(user);
|
||||
|
||||
// 生成令牌
|
||||
String accessToken = jwtUtils.generateToken(user.getId(), user.getUsername());
|
||||
String refreshToken = RandomUtil.randomString(32);
|
||||
|
||||
// 存储刷新令牌
|
||||
redisUtils.set(USER_TOKEN_PREFIX + user.getId(), refreshToken, expiration * 2);
|
||||
|
||||
// 缓存用户角色和权限
|
||||
cacheUserAuthInfo(user.getId());
|
||||
|
||||
// 构建返回结果
|
||||
TokenInfoDTO tokenInfo = new TokenInfoDTO()
|
||||
.setAccessToken(accessToken)
|
||||
.setRefreshToken(refreshToken)
|
||||
.setTokenType("Bearer")
|
||||
.setExpiresIn(expiration)
|
||||
.setUserId(user.getId())
|
||||
.setUsername(user.getUsername());
|
||||
|
||||
return Result.success(tokenInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<String> register(RegisterDTO registerDTO) {
|
||||
// 验证验证码
|
||||
String captchaCache = (String) redisUtils.get(CAPTCHA_PREFIX + registerDTO.getCaptchaKey());
|
||||
if (captchaCache == null) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码已过期");
|
||||
}
|
||||
if (!registerDTO.getCaptcha().equalsIgnoreCase(captchaCache)) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "验证码错误");
|
||||
}
|
||||
|
||||
// 验证两次密码输入是否一致
|
||||
if (!registerDTO.getPassword().equals(registerDTO.getConfirmPassword())) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "两次密码输入不一致");
|
||||
}
|
||||
|
||||
// 验证用户名是否已存在
|
||||
AuthUser existUser = getByUsername(registerDTO.getUsername());
|
||||
if (existUser != null) {
|
||||
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "用户名已存在");
|
||||
}
|
||||
|
||||
// 验证邮箱是否已存在
|
||||
if (StringUtils.hasText(registerDTO.getEmail())) {
|
||||
Long count = count(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getEmail, registerDTO.getEmail()));
|
||||
if (count > 0) {
|
||||
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "邮箱已被注册");
|
||||
}
|
||||
}
|
||||
|
||||
// 验证手机号是否已存在
|
||||
if (StringUtils.hasText(registerDTO.getPhone())) {
|
||||
Long count = count(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getPhone, registerDTO.getPhone()));
|
||||
if (count > 0) {
|
||||
return Result.error(ResultCode.USER_ALREADY_EXISTS.getCode(), "手机号已被注册");
|
||||
}
|
||||
}
|
||||
|
||||
// 移除验证码
|
||||
redisUtils.del(CAPTCHA_PREFIX + registerDTO.getCaptchaKey());
|
||||
|
||||
// 创建用户
|
||||
AuthUser user = new AuthUser();
|
||||
user.setUsername(registerDTO.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
|
||||
user.setSalt(RandomUtil.randomString(6));
|
||||
user.setEmail(registerDTO.getEmail());
|
||||
user.setPhone(registerDTO.getPhone());
|
||||
user.setStatus(1);
|
||||
user.setCreatedBy("system");
|
||||
user.setUpdatedBy("system");
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
// 保存用户
|
||||
save(user);
|
||||
|
||||
// 分配默认角色(普通用户)
|
||||
AuthUserRole userRole = new AuthUserRole();
|
||||
userRole.setUserId(user.getId());
|
||||
userRole.setRoleId(2L); // 普通用户角色ID
|
||||
userRole.setCreatedBy("system");
|
||||
userRole.setCreateTime(LocalDateTime.now());
|
||||
authUserRoleMapper.insert(userRole);
|
||||
|
||||
return Result.success("注册成功");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthUser getByUsername(String username) {
|
||||
return getOne(new LambdaQueryWrapper<AuthUser>().eq(AuthUser::getUsername, username));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<TokenInfoDTO> refreshToken(String refreshToken) {
|
||||
try {
|
||||
if (!StringUtils.hasText(refreshToken)) {
|
||||
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌无效");
|
||||
}
|
||||
|
||||
// 解析accessToken获取用户ID
|
||||
String accessToken = jwtUtils.getUserIdFromToken(refreshToken).toString();
|
||||
Long userId = Long.parseLong(accessToken);
|
||||
|
||||
// 验证refreshToken是否存在且匹配
|
||||
String cacheRefreshToken = (String) redisUtils.get(USER_TOKEN_PREFIX + userId);
|
||||
if (cacheRefreshToken == null || !cacheRefreshToken.equals(refreshToken)) {
|
||||
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌无效");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
AuthUser user = getById(userId);
|
||||
if (user == null || user.getStatus() != 1) {
|
||||
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "用户不存在或已被禁用");
|
||||
}
|
||||
|
||||
// 生成新令牌
|
||||
String newAccessToken = jwtUtils.generateToken(user.getId(), user.getUsername());
|
||||
String newRefreshToken = RandomUtil.randomString(32);
|
||||
|
||||
// 更新刷新令牌
|
||||
redisUtils.set(USER_TOKEN_PREFIX + user.getId(), newRefreshToken, expiration * 2);
|
||||
|
||||
// 构建返回结果
|
||||
TokenInfoDTO tokenInfo = new TokenInfoDTO()
|
||||
.setAccessToken(newAccessToken)
|
||||
.setRefreshToken(newRefreshToken)
|
||||
.setTokenType("Bearer")
|
||||
.setExpiresIn(expiration)
|
||||
.setUserId(user.getId())
|
||||
.setUsername(user.getUsername());
|
||||
|
||||
return Result.success(tokenInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("刷新令牌失败", e);
|
||||
return Result.error(ResultCode.UNAUTHORIZED.getCode(), "刷新令牌失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Void> logout(Long userId) {
|
||||
if (userId == null) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "用户ID不能为空");
|
||||
}
|
||||
|
||||
// 清除用户令牌
|
||||
redisUtils.del(USER_TOKEN_PREFIX + userId);
|
||||
|
||||
// 清除用户权限缓存
|
||||
redisUtils.del(USER_ROLE_PREFIX + userId);
|
||||
redisUtils.del(USER_PERMISSION_PREFIX + userId);
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<AuthUser> getCurrentUser(Long userId) {
|
||||
if (userId == null) {
|
||||
return Result.error(ResultCode.PARAM_ERROR.getCode(), "用户ID不能为空");
|
||||
}
|
||||
|
||||
AuthUser user = getById(userId);
|
||||
if (user == null) {
|
||||
return Result.error(ResultCode.USER_NOT_FOUND.getCode(), "用户不存在");
|
||||
}
|
||||
|
||||
// 脱敏处理
|
||||
user.setPassword(null);
|
||||
user.setSalt(null);
|
||||
|
||||
// 获取角色信息
|
||||
List<String> roles = getUserRoles(userId);
|
||||
user.setRoles(roles.toArray(new String[0]));
|
||||
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色列表
|
||||
*/
|
||||
private List<String> getUserRoles(Long userId) {
|
||||
// 优先从缓存获取
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> roles = (List<String>) redisUtils.get(USER_ROLE_PREFIX + userId);
|
||||
if (roles == null || roles.isEmpty()) {
|
||||
// 从数据库查询并缓存
|
||||
roles = authUserMapper.getUserRoles(userId);
|
||||
if (!roles.isEmpty()) {
|
||||
redisUtils.set(USER_ROLE_PREFIX + userId, roles, expiration);
|
||||
}
|
||||
}
|
||||
return roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 权限列表
|
||||
*/
|
||||
private List<String> getUserPermissions(Long userId) {
|
||||
// 优先从缓存获取
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> permissions = (List<String>) redisUtils.get(USER_PERMISSION_PREFIX + userId);
|
||||
if (permissions == null || permissions.isEmpty()) {
|
||||
// 从数据库查询并缓存
|
||||
permissions = authUserMapper.getUserPermissions(userId);
|
||||
if (!permissions.isEmpty()) {
|
||||
redisUtils.set(USER_PERMISSION_PREFIX + userId, permissions, expiration);
|
||||
}
|
||||
}
|
||||
return permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存用户认证信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
private void cacheUserAuthInfo(Long userId) {
|
||||
// 缓存用户角色
|
||||
List<String> roles = authUserMapper.getUserRoles(userId);
|
||||
if (!roles.isEmpty()) {
|
||||
redisUtils.set(USER_ROLE_PREFIX + userId, roles, expiration);
|
||||
}
|
||||
|
||||
// 缓存用户权限
|
||||
List<String> permissions = authUserMapper.getUserPermissions(userId);
|
||||
if (!permissions.isEmpty()) {
|
||||
redisUtils.set(USER_PERMISSION_PREFIX + userId, permissions, expiration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.nuaa.video.auth.service.impl;
|
||||
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import com.nuaa.video.auth.service.CaptchaService;
|
||||
import com.nuaa.video.common.model.vo.Result;
|
||||
import com.nuaa.video.auth.utils.RedisUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 验证码服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaServiceImpl implements CaptchaService {
|
||||
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Value("${captcha.expiration:60}")
|
||||
private long captchaExpiration;
|
||||
|
||||
private static final String CAPTCHA_PREFIX = "captcha:";
|
||||
|
||||
@Override
|
||||
public Result<Map<String, Object>> generateCaptcha() {
|
||||
try {
|
||||
// 创建验证码
|
||||
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 40, 4, 10);
|
||||
|
||||
// 获取验证码内容和图片
|
||||
String code = captcha.getCode();
|
||||
|
||||
// 生成验证码key
|
||||
String captchaKey = RandomUtil.randomString(16);
|
||||
|
||||
// 将验证码存入Redis
|
||||
redisUtils.set(CAPTCHA_PREFIX + captchaKey, code, captchaExpiration);
|
||||
|
||||
// 转换验证码图片为Base64
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
captcha.write(outputStream);
|
||||
String captchaBase64 = "data:image/png;base64," + Base64.encode(outputStream.toByteArray());
|
||||
|
||||
// 构建返回信息
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("captchaKey", captchaKey);
|
||||
result.put("captchaImage", captchaBase64);
|
||||
|
||||
return Result.success(result);
|
||||
} catch (Exception e) {
|
||||
log.error("生成验证码失败", e);
|
||||
return Result.error("生成验证码失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.nuaa.video.auth.utils;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* JWT工具类 - auth模块专用
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
|
||||
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400}")
|
||||
private long expiration;
|
||||
|
||||
@Value("${jwt.token-header:Authorization}")
|
||||
private String tokenHeader;
|
||||
|
||||
/**
|
||||
* 根据用户ID生成JWT
|
||||
*/
|
||||
public String generateToken(Long userId, String username) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
return createToken(claims, userId.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
return Long.valueOf(getClaimFromToken(token, Claims::getSubject));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断JWT是否过期
|
||||
*/
|
||||
public boolean isTokenExpired(String token) {
|
||||
final Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证JWT
|
||||
*/
|
||||
public boolean validateToken(String token, String username) {
|
||||
try {
|
||||
final String tokenUsername = getUsernameFromToken(token);
|
||||
return (tokenUsername.equals(username) && !isTokenExpired(token));
|
||||
} catch (Exception e) {
|
||||
log.error("JWT验证失败:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT中获取用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
return getClaimFromToken(token, claims -> claims.get("username", String.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取JWT过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT中获取指定声明
|
||||
*/
|
||||
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = getAllClaimsFromToken(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JWT中获取所有声明
|
||||
*/
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建JWT
|
||||
*/
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.subject(subject)
|
||||
.issuedAt(new Date(System.currentTimeMillis()))
|
||||
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.nuaa.video.auth.utils;
|
||||
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis工具类 - auth模块专用
|
||||
*/
|
||||
@Component
|
||||
public class RedisUtils {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缓存过期时间
|
||||
*/
|
||||
public boolean expire(String key, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.expire(key, time, TimeUnit.SECONDS);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存过期时间
|
||||
*/
|
||||
public long getExpire(String key) {
|
||||
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断key是否存在
|
||||
*/
|
||||
public boolean hasKey(String key) {
|
||||
try {
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存
|
||||
*/
|
||||
public void del(String... key) {
|
||||
if (key != null && key.length > 0) {
|
||||
if (key.length == 1) {
|
||||
redisTemplate.delete(key[0]);
|
||||
} else {
|
||||
redisTemplate.delete((Collection<String>) List.of(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取普通缓存
|
||||
*/
|
||||
public Object get(String key) {
|
||||
return key == null ? null : redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置普通缓存
|
||||
*/
|
||||
public boolean set(String key, Object value) {
|
||||
try {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置普通缓存并设置过期时间
|
||||
*/
|
||||
public boolean set(String key, Object value, long time) {
|
||||
try {
|
||||
if (time > 0) {
|
||||
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
|
||||
} else {
|
||||
set(key, value);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 其他必要的方法可以根据需要添加
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
@@ -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
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.nuaa.video.auth.mapper.AuthPermissionMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.nuaa.video.auth.mapper.AuthRoleMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
|
||||
<mapper namespace="com.nuaa.video.auth.mapper.AuthRolePermissionMapper">
|
||||
</mapper>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.nuaa.video.auth.mapper.AuthUserMapper">
|
||||
|
||||
<!-- 获取用户角色 -->
|
||||
<select id="getUserRoles" resultType="string">
|
||||
SELECT
|
||||
r.code
|
||||
FROM
|
||||
auth_user_role ur
|
||||
INNER JOIN auth_role r ON ur.role_id = r.id
|
||||
WHERE
|
||||
ur.user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<!-- 获取用户权限 -->
|
||||
<select id="getUserPermissions" resultType="string">
|
||||
SELECT
|
||||
p.code
|
||||
FROM
|
||||
auth_user_role ur
|
||||
INNER JOIN auth_role_permission rp ON ur.role_id = rp.role_id
|
||||
INNER JOIN auth_permission p ON rp.permission_id = p.id
|
||||
WHERE
|
||||
ur.user_id = #{userId}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -1,4 +1,3 @@
|
||||
// com.nuaa.video.common.config.MybatisPlusConfig.java
|
||||
package com.nuaa.video.common.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
|
||||
Reference in New Issue
Block a user