feat(auth): Implement authentication module foundation

This commit is contained in:
2025-03-31 02:21:04 +08:00
parent 2d0cc592e8
commit 8c4d8e4c6a
37 changed files with 2072 additions and 13 deletions
Binary file not shown.
+7
View File
@@ -2,5 +2,12 @@
<project version="4">
<component name="ApifoxUploaderProjectSetting">
<option name="apiAccessToken" value="APS-8W5ece3ancukaCFg7XU25oGRku4hR3r5" />
<option name="apiProjectIds">
<array>
<option value="&lt;byte-array&gt;rO0ABXNyADZjb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50LlByb2plY3RBbmRNb2R1bGUAAAAAAAAAAQIAFVoABmVuYWJsZUwACG1vZHVsZUlkdAASTGphdmEvbGFuZy9TdHJpbmc7TAAGb3RoZXIxcQB+AAFMAAdvdGhlcjEwcQB+AAFMAAdvdGhlcjExcQB+AAFMAAdvdGhlcjEycQB+AAFMAAZvdGhlcjJxAH4AAUwABm90aGVyM3EAfgABTAAGb3RoZXI0cQB+AAFMAAZvdGhlcjVxAH4AAUwABm90aGVyNnEAfgABTAAGb3RoZXI3cQB+AAFMAAZvdGhlcjhxAH4AAUwABm90aGVyOXEAfgABTAAKcGF0aEJlZm9yZXEAfgABTAANcHJvamVjdEZvbGRlcnEAfgABTAAPcHJvamVjdEZvbGRlcklkcQB+AAFMAAlwcm9qZWN0SWRxAH4AAUwAC3Byb2plY3ROYW1lcQB+AAFMAAxzY2hlbWFGb2xkZXJxAH4AAUwACHNjaGVtYUlkcQB+AAF4cAF0AAp2aWRlby1hdXRocHBwcHBwcHBwcHBwdAAAdAAKdmlkZW8tYXV0aHQACzUuMzQ1NTAwMUU3dAAJNjEyNjYzNC4wdAAGSEEtT1ZQdAAJ5qC555uu5b2VdAABMA==&lt;/byte-array&gt;" />
</array>
</option>
<option name="treeNodes" value="&lt;byte-array&gt;rO0ABXNyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNzT3JkZXJ4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAAXQABzI2NTY1MjFzcgAuY29tLml0YW5nY2VudC5pZGVhLnBsdWdpbi5hcGkuYWNjb3VudC5UcmVlTm9kZQAAAAAAAAABAgALTAAHYWxsUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACGNoaWxkcmVudAAPTGphdmEvdXRpbC9NYXA7TAAIZnVsbFBhdGhxAH4ABUwAA2tleXEAfgAFTAAEbmFtZXEAfgAFTAAIcGFyZW50SWRxAH4ABUwACXByb2plY3RJZHEAfgAFTAALcHJvamVjdE5hbWVxAH4ABUwABnRlYW1JZHEAfgAFTAAIdGVhbU5hbWVxAH4ABUwABHR5cGV0ADBMY29tL2l0YW5nY2VudC9pZGVhL3BsdWdpbi9hcGkvYWNjb3VudC9Ob2RlVHlwZTt4cHQADOS4quS6uuWboumYn3NxAH4AAD9AAAAAAAAMdwgAAAAQAAAABHQABzQ2NTczMjZzcQB+AAR0ABnkuKrkurrlm6LpmJ8v5Liq5Lq66aG555uuc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcQB+AAt0ABbkuKrkurrpobnnm64gKDQ2NTczMjYpdAAHMjY1NjUyMXQABzQ2NTczMjZ0AAzkuKrkurrpobnnm650AAcyNjU2NTIxcH5yAC5jb20uaXRhbmdjZW50LmlkZWEucGx1Z2luLmFwaS5hY2NvdW50Lk5vZGVUeXBlAAAAAAAAAAASAAB4cgAOamF2YS5sYW5nLkVudW0AAAAAAAAAABIAAHhwdAAHUFJPSkVDVHQABzQ2ODIxMTBzcQB+AAR0ABbkuKrkurrlm6LpmJ8veXF4cy1zZWxmc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeABwcQB+ABh0ABN5cXhzLXNlbGYgKDQ2ODIxMTApdAAHMjY1NjUyMXQABzQ2ODIxMTB0AAl5cXhzLXNlbGZ0AAcyNjU2NTIxcHEAfgAWdAAHNDc1NzQ0NHNxAH4ABHQAFuS4quS6uuWboumYny95cXhzLXN5bmNzcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4AHBxAH4AIXQAE3lxeHMtc3luYyAoNDc1NzQ0NCl0AAcyNjU2NTIxdAAHNDc1NzQ0NHQACXlxeHMtc3luY3QABzI2NTY1MjFwcQB+ABZ0AAc2MTI2NjM0c3EAfgAEdAAT5Liq5Lq65Zui6ZifL0hBLU9WUHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAAXQACzUuMzQ1NTAwMUU3c3EAfgAEdAAe5Liq5Lq65Zui6ZifL0hBLU9WUC92aWRlby1hdXRoc3EAfgAAP0AAAAAAAAB3CAAAABAAAAAAeAB0AAp2aWRlby1hdXRocQB+AC50AAp2aWRlby1hdXRodAAHNjEyNjYzNHQACTYxMjY2MzQuMHQABkhBLU9WUHBwfnEAfgAUdAAGRk9MREVSeABwcQB+ACp0ABBIQS1PVlAgKDYxMjY2MzQpdAAHMjY1NjUyMXQABzYxMjY2MzR0AAZIQS1PVlB0AAcyNjU2NTIxcHEAfgAWeABwcQB+AAN0AAzkuKrkurrlm6LpmJ9wcHB0AAcyNjU2NTIxdAAM5Liq5Lq65Zui6ZiffnEAfgAUdAAEVEVBTXgA&lt;/byte-array&gt;" />
<option name="treeNodesJTree" value="&lt;byte-array&gt;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=&lt;/byte-array&gt;" />
</component>
</project>
+23
View File
@@ -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
View File
@@ -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
+62 -10
View File
@@ -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-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;