【feat】 初步实现了账号密码登录

This commit is contained in:
Kris 2025-12-21 15:06:38 +08:00
parent 39b80c307a
commit e50dfc2304
9 changed files with 2714 additions and 0 deletions

View File

@ -0,0 +1,204 @@
# Salesforce登录接口说明文档
## 1. 概述
本文档详细描述了系统中提供的四种Salesforce登录策略的使用方法包括每种策略所需的参数、调用方式以及异常处理机制。系统采用策略模式设计方便扩展和维护。
## 2. 登录策略概览
系统目前支持以下四种登录策略:
| 策略名称 | 登录类型(loginType) | 描述 |
|---------|-------------------|------|
| OAuth2LoginStrategy | oauth2 | 支持OAuth 2.0的三种授权模式 |
| LegacyCredentialLoginStrategy | legacy_credential | 使用用户名、密码和安全令牌的传统登录方式 |
| SessionIdLoginStrategy | session_id | 使用已有的Salesforce Session ID进行登录 |
| SalesforceCliLoginStrategy | salesforce_cli | 通过Salesforce CLI工具获取访问令牌 |
## 3. 各登录策略详细说明
### 3.1 OAuth2LoginStrategy (oauth2)
#### 3.1.1 支持的授权模式
该策略支持以下三种OAuth 2.0授权模式:
1. **密码模式 (Password Grant)** - 直接使用用户名和密码获取访问令牌
2. **客户端凭证模式 (Client Credentials Grant)** - 使用客户端凭证获取访问令牌
3. **授权码模式 (Authorization Code Grant)** - 通过用户授权获取访问令牌
#### 3.1.2 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|-------|-----|-----|------|
| loginType | String | 是 | 固定值:"oauth2" |
| grantType | String | 是 | 授权类型:"password"、"client_credentials"或"authorization_code" |
| username | String | 条件 | 密码模式必填 |
| password | String | 条件 | 密码模式必填 |
| securityToken | String | 条件 | 密码模式时的安全令牌 |
| clientId | String | 是 | 应用的客户端ID |
| clientSecret | String | 是 | 应用的客户端密钥 |
| code | String | 条件 | 授权码模式必填 |
| state | String | 条件 | 授权码模式必填用于防止CSRF攻击 |
#### 3.1.3 调用示例
##### 密码模式示例:
```java
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("oauth2");
request.setGrantType("password");
request.setUsername("user@example.com");
request.setPassword("password");
request.setSecurityToken("securityToken");
request.setClientId("your_client_id");
request.setClientSecret("your_client_secret");
SalesforceLoginResult result = loginService.login(request);
```
##### 客户端凭证模式示例:
```java
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("oauth2");
request.setGrantType("client_credentials");
request.setClientId("your_client_id");
request.setClientSecret("your_client_secret");
SalesforceLoginResult result = loginService.login(request);
```
##### 授权码模式示例:
```java
// 第一步生成授权URL并引导用户访问
// 第二步用户授权后回调获取code和state参数
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("oauth2");
request.setGrantType("authorization_code");
request.setCode("authorization_code_from_callback");
request.setState("state_from_callback");
request.setClientId("your_client_id");
request.setClientSecret("your_client_secret");
SalesforceLoginResult result = loginService.login(request);
```
#### 3.1.4 异常处理
| 异常类 | 错误码 | 说明 |
|--------|-------|------|
| SalesforceOAuthException | OAUTH2_UNSUPPORTED_GRANT_TYPE | 不支持的授权类型 |
| SalesforceOAuthException | OAUTH2_MISSING_REFRESH_TOKEN | 缺少刷新令牌 |
| SalesforceOAuthException | OAUTH2_CONFIG_ERROR | 配置错误 |
| SalesforceOAuthException | OAUTH2_MISSING_CLIENT_ID | 缺少客户端ID |
| SalesforceOAuthException | OAUTH2_MISSING_CLIENT_SECRET | 缺少客户端密钥 |
| SalesforceOAuthException | OAUTH2_MISSING_AUTHORIZATION_CODE | 缺少授权码 |
| SalesforceOAuthException | OAUTH2_MISSING_STATE | 缺少state参数 |
| SalesforceOAuthException | OAUTH2_INVALID_STATE | 无效的state参数 |
| SalesforceOAuthException | OAUTH2_EXPIRED_STATE | state参数已过期 |
### 3.2 LegacyCredentialLoginStrategy (legacy_credential)
#### 3.2.1 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|-------|-----|-----|------|
| loginType | String | 是 | 固定值:"legacy_credential" |
| username | String | 是 | Salesforce用户名 |
| password | String | 是 | Salesforce密码 |
| securityToken | String | 否 | 安全令牌根据Salesforce设置可能必需 |
#### 3.2.2 调用示例
```java
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("legacy_credential");
request.setUsername("user@example.com");
request.setPassword("password");
request.setSecurityToken("securityToken");
SalesforceLoginResult result = loginService.login(request);
```
#### 3.2.3 异常处理
| 异常类 | 错误码 | 说明 |
|--------|-------|------|
| SalesforceLegacyCredentialLoginException | MISSING_USERNAME | 缺少用户名 |
| SalesforceLegacyCredentialLoginException | MISSING_PASSWORD | 缺少密码 |
| SalesforceLegacyCredentialLoginException | CONFIG_NOT_FOUND | 配置未找到 |
| SalesforceLegacyCredentialLoginException | SOAP_FAULT | SOAP故障 |
### 3.3 SessionIdLoginStrategy (session_id)
#### 3.3.1 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|-------|-----|-----|------|
| loginType | String | 是 | 固定值:"session_id" |
| sessionId | String | 是 | Salesforce Session ID |
#### 3.3.2 调用示例
```java
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("session_id");
request.setSessionId("00Dxx0000000000!ARUAQH7...");
SalesforceLoginResult result = loginService.login(request);
```
#### 3.3.3 异常处理
| 异常类 | 错误码 | 说明 |
|--------|-------|------|
| SalesforceSessionIdLoginException | SESSION_ID_EMPTY | Session ID为空 |
| SalesforceSessionIdLoginException | CONFIG_NOT_FOUND | 配置未找到 |
| SalesforceSessionIdLoginException | INVALID_SESSION_ID | 无效的Session ID |
### 3.4 SalesforceCliLoginStrategy (salesforce_cli)
#### 3.4.1 参数说明
| 参数名 | 类型 | 必填 | 说明 |
|-------|-----|-----|------|
| loginType | String | 是 | 固定值:"salesforce_cli" |
| orgAlias | String | 否 | Salesforce组织别名如果不提供则使用默认组织 |
#### 3.4.2 调用示例
```java
SalesforceLoginRequest request = new SalesforceLoginRequest();
request.setLoginType("salesforce_cli");
request.setOrgAlias("your_org_alias"); // 可选
SalesforceLoginResult result = loginService.login(request);
```
#### 3.4.3 异常处理
| 异常类 | 错误码 | 说明 |
|--------|-------|------|
| SalesforceCliLoginException | CLI_NOT_INSTALLED | Salesforce CLI未安装 |
| SalesforceCliLoginException | CLI_COMMAND_FAILED | CLI命令执行失败 |
| SalesforceCliLoginException | CLI_PARSE_ERROR | CLI输出解析错误 |
## 4. 返回结果说明
所有登录方法都会返回[SalesforceLoginResult](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/domain/SalesforceLoginResult.java#L15-L152)对象,包含以下字段:
| 字段名 | 类型 | 说明 |
|-------|-----|------|
| success | boolean | 登录是否成功 |
| accessToken | String | 访问令牌 |
| refreshToken | String | 刷新令牌 |
| instanceUrl | String | Salesforce实例URL |
| organizationId | String | 组织ID |
| userId | String | 用户ID |
| tokenType | String | 令牌类型(通常是"Bearer" |
| expiresIn | long | 令牌过期时间(秒) |
| errorMessage | String | 错误消息 |
| errorCode | String | 错误码 |

View File

@ -0,0 +1,488 @@
# Salesforce登录认证接口调用说明文档
## 1. 概述
本文档详细描述了Salesforce登录认证模块提供的RESTful API接口包括各接口的功能、请求方式、参数说明、返回结果以及调用示例。
### 1.1 基础信息
- 基础路径:`/salesforce/login`
- 控制器类:`DataISfLoginController`
- 跨域支持:是
## 2. 接口详情
### 2.1 执行登录
#### 接口描述
根据不同登录类型执行Salesforce登录操作。
#### 请求URL
```
POST /salesforce/login/execute
```
#### 请求头
```
Content-Type: application/json
```
#### 请求体参数 (SalesforceLoginRequest)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| loginType | String | 是 | 登录类型支持的类型有oauth2、salesforce_cli、legacy_credential、session_id |
| username | String | 否 | 用户名 |
| password | String | 否 | 密码 |
| securityToken | String | 否 | 安全令牌 |
| clientId | String | 否 | OAuth客户端ID |
| clientSecret | String | 否 | OAuth客户端密钥 |
| grantType | String | 否 | OAuth授权类型 |
| orgAlias | String | 否 | Salesforce CLI组织别名 |
| privateKeyPath | String | 否 | 私有密钥路径 |
| privateKeyPassword | String | 否 | 私有密钥密码 |
| code | String | 否 | OAuth授权码 |
| sessionId | String | 否 | Salesforce Session ID |
#### 不同登录类型所需参数说明
##### 1. oauth2 登录类型
使用OAuth 2.0标准方式进行登录,适用于大多数现代应用集成场景。
必需参数:
- loginType: "oauth2"
- username: 用户名
- password: 密码
- clientId: OAuth客户端ID
- clientSecret: OAuth客户端密钥
- grantType: 授权类型,支持 "password"、"authorization_code" 或 "client_credentials"
示例请求:
```json
{
"loginType": "oauth2",
"username": "user@example.com",
"password": "password123",
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"grantType": "password"
}
```
##### 2. salesforce_cli 登录类型
使用Salesforce CLI已经登录的会话进行身份验证适用于开发和测试环境。
必需参数:
- loginType: "salesforce_cli"
- username: 用户名
- orgAlias: (可选) 已登录的CLI组织别名
示例请求:
```json
{
"loginType": "salesforce_cli",
"username": "user@example.com",
"orgAlias": "my-sandbox"
}
```
##### 3. legacy_credential 登录类型
使用传统的用户名、密码和安全令牌组合进行登录,适用于较老的系统集成。
必需参数:
- loginType: "legacy_credential"
- username: 用户名
- password: 密码
- securityToken: 安全令牌
示例请求:
```json
{
"loginType": "legacy_credential",
"username": "user@example.com",
"password": "password123",
"securityToken": "securityToken123"
}
```
##### 4. session_id 登录类型
使用已有的Salesforce Session ID进行登录适用于已有有效会话的场景。
必需参数:
- loginType: "session_id"
- sessionId: Salesforce Session ID
示例请求:
```json
{
"loginType": "session_id",
"sessionId": "00Dxx0000001gEWEAY!ARwAQ.KBMrn123XYZ"
}
```
#### 请求示例
```
{
"loginType": "oauth2",
"username": "user@example.com",
"password": "password123",
"clientId": "your_client_id",
"clientSecret": "your_client_secret",
"grantType": "password"
}
```
#### 成功响应示例
```
{
"code": 200,
"msg": "登录成功",
"data": {
"success": true,
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
"instanceUrl": "https://your-instance.salesforce.com",
"organizationId": "00Dxx0000001gEWEAY",
"userId": "005xx000001SwiQAAS",
"tokenType": "Bearer",
"expiresIn": 7200
}
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "登录失败: Invalid username, password, security token; or user locked out."
}
```
---
### 2.2 刷新令牌
#### 接口描述
使用刷新令牌获取新的访问令牌。
#### 请求URL
```
POST /salesforce/login/refresh-token
```
#### 请求参数 (@RequestParam)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| refreshToken | String | 是 | 刷新令牌 |
| loginType | String | 是 | 登录类型支持的类型有oauth2、salesforce_cli、legacy_credential、session_id |
#### 请求示例
```
POST /salesforce/login/refresh-token?refreshToken=5Aep861z1k8KCNBg3KmH1...&loginType=oauth2
```
#### 成功响应示例
```
{
"code": 200,
"msg": "令牌刷新成功",
"data": {
"success": true,
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
"instanceUrl": "https://your-instance.salesforce.com",
"organizationId": "00Dxx0000001gEWEAY",
"userId": "005xx000001SwiQAAS",
"tokenType": "Bearer",
"expiresIn": 7200
}
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "刷新令牌失败: expired authorization code"
}
```
---
### 2.3 执行登出
#### 接口描述
退出登录,清理登录状态。
#### 请求URL
```
POST /salesforce/login/logout
```
#### 请求参数 (@RequestParam)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| accessToken | String | 是 | 访问令牌 |
| loginType | String | 是 | 登录类型支持的类型有oauth2、salesforce_cli、legacy_credential、session_id |
#### 请求示例
```
POST /salesforce/login/logout?accessToken=00Dxx0000001gEWEAY!ARwAQ...&loginType=oauth2
```
#### 成功响应示例
```
{
"code": 200,
"msg": "登出成功"
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "登出失败"
}
```
---
### 2.4 获取当前登录状态
#### 接口描述
获取当前系统的登录状态。
#### 请求URL
```
GET /salesforce/login/status
```
#### 请求示例
```
GET /salesforce/login/status
```
#### 成功响应示例
```
{
"code": 200,
"msg": "获取成功",
"data": {
"success": true,
"accessToken": "00Dxx0000001gEWEAY!ARwAQ...",
"refreshToken": "5Aep861z1k8KCNBg3KmH1...",
"instanceUrl": "https://your-instance.salesforce.com",
"organizationId": "00Dxx0000001gEWEAY",
"userId": "005xx000001SwiQAAS",
"tokenType": "Bearer",
"expiresIn": 7200
}
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "获取登录状态失败: 未找到有效的登录状态"
}
```
---
### 2.5 验证令牌
#### 接口描述
验证访问令牌的有效性。
#### 请求URL
```
GET /salesforce/login/validate-token
```
#### 请求参数 (@RequestParam)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| accessToken | String | 是 | 访问令牌 |
#### 请求示例
```
GET /salesforce/login/validate-token?accessToken=00Dxx0000001gEWEAY!ARwAQ...
```
#### 成功响应示例
```
{
"code": 200,
"msg": "令牌有效"
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "令牌无效"
}
```
---
### 2.6 绑定令牌
#### 接口描述
将令牌绑定到指定设备和IP。
#### 请求URL
```
POST /salesforce/login/bind-token
```
#### 请求参数 (@RequestParam)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| accessToken | String | 是 | 访问令牌 |
| deviceId | String | 否 | 设备ID |
| ip | String | 否 | IP地址 |
#### 请求示例
```
POST /salesforce/login/bind-token?accessToken=00Dxx0000001gEWEAY!ARwAQ...&deviceId=device123&ip=192.168.1.100
```
#### 成功响应示例
```
{
"code": 200,
"msg": "令牌绑定成功"
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "绑定令牌失败: 令牌不存在"
}
```
---
### 2.7 检查令牌绑定
#### 接口描述
检查令牌是否与指定设备和IP匹配。
#### 请求URL
```
GET /salesforce/login/check-binding
```
#### 请求参数 (@RequestParam)
| 参数名 | 类型 | 必填 | 描述 |
|--------|------|------|------|
| accessToken | String | 是 | 访问令牌 |
| deviceId | String | 否 | 设备ID |
| ip | String | 否 | IP地址 |
#### 请求示例
```
GET /salesforce/login/check-binding?accessToken=00Dxx0000001gEWEAY!ARwAQ...&deviceId=device123&ip=192.168.1.100
```
#### 成功响应示例
```
{
"code": 200,
"msg": "令牌绑定匹配"
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "令牌绑定不匹配"
}
```
---
### 2.8 获取当前会话信息
#### 接口描述
获取当前登录成功的会话详细信息。
#### 请求URL
```
GET /salesforce/login/session
```
#### 请求示例
```
GET /salesforce/login/session
```
#### 成功响应示例
```
{
"code": 200,
"msg": "获取成功",
"data": {
"sessionId": 1,
"tenantId": "default",
"username": "user@example.com",
"loginType": "oauth2",
"status": "active",
"loginTime": "2025-12-14T10:30:00",
"expireTime": "2025-12-14T12:30:00",
"lastActivityTime": "2025-12-14T10:35:00",
"loginIp": "192.168.1.100",
"deviceInfo": "Windows PC",
"browserInfo": "Chrome 98.0.4758.102",
"sessionToken": "sess_abc123xyz",
"tokenId": 1001
}
}
```
#### 失败响应示例
```
{
"code": 500,
"msg": "当前没有活跃的登录会话"
}
```
## 3. 返回码说明
| 返回码 | 说明 |
|--------|------|
| 200 | 操作成功 |
| 500 | 系统内部错误 |
## 4. 注意事项
1. 所有接口均支持跨域调用
2. 登录成功后系统会自动将登录结果缓存到Redis中
3. 令牌刷新成功后,系统会自动更新缓存中的登录状态
4. 登出操作会清理相关的登录状态缓存
5. 令牌绑定功能可以增强系统安全性,防止令牌被盗用
6. 所有敏感操作都会记录日志,便于审计追踪
## 5. 错误处理
当发生错误时,系统会返回统一的错误格式:
```
{
"code": 错误码,
"msg": "错误描述信息"
}
```
常见错误情况包括但不限于:
- 参数校验失败
- 网络连接异常
- Salesforce API调用失败
- 缓存服务不可用
- 数据库操作异常

View File

@ -0,0 +1,160 @@
# 获取当前会话信息接口说明文档
## 1. 接口概述
### 1.1 接口名称
获取当前会话信息
### 1.2 接口描述
该接口用于获取当前用户登录成功的会话详细信息,包括用户基本信息、登录时间、会话状态等。通过此接口可以验证用户是否已登录以及获取当前登录会话的详细数据。
### 1.3 请求URL
```
GET /salesforce/login/session
```
### 1.4 请求方法
GET
### 1.5 是否需要认证
否(但需确保用户已登录)
## 2. 请求参数
该接口无需传递任何请求参数。
## 3. 响应格式
### 3.1 响应结构
```json
{
"code": 200,
"msg": "获取成功",
"data": {
// 会话详细信息对象
}
}
```
### 3.2 响应字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| code | Integer | 响应状态码200表示成功 |
| msg | String | 响应消息 |
| data | Object | 响应数据,包含会话详细信息 |
### 3.3 会话信息对象字段说明
| 字段名 | 类型 | 说明 |
|--------|------|------|
| sessionId | Long | 会话ID |
| tenantId | String | 租户编号 |
| username | String | 用户名 |
| loginType | String | 登录类型oauth2、salesforce_cli、legacy_credential、session_id |
| status | String | 会话状态ACTIVE、EXPIRED等 |
| loginTime | String | 登录时间ISO 8601格式 |
| expireTime | String | 过期时间ISO 8601格式 |
| lastActivityTime | String | 最后活动时间ISO 8601格式 |
| loginIp | String | 登录IP地址 |
| deviceInfo | String | 设备信息 |
| browserInfo | String | 浏览器信息 |
| sessionToken | String | 会话标识 |
| tokenId | Long | 令牌ID |
## 4. 使用示例
### 4.1 请求示例
```bash
curl -X GET "http://localhost:8080/salesforce/login/session" \
-H "Content-Type: application/json"
```
### 4.2 成功响应示例
```json
{
"code": 200,
"msg": "获取成功",
"data": {
"sessionId": 10001,
"tenantId": "default",
"username": "user@example.com",
"loginType": "oauth2",
"status": "ACTIVE",
"loginTime": "2025-12-14T10:30:00",
"expireTime": "2025-12-14T12:30:00",
"lastActivityTime": "2025-12-14T10:35:00",
"loginIp": "192.168.1.100",
"deviceInfo": "Windows PC",
"browserInfo": "Chrome 98.0.4758.102",
"sessionToken": "sess_abc123xyz",
"tokenId": 20001
}
}
```
### 4.3 失败响应示例(无活跃会话)
```json
{
"code": 500,
"msg": "当前没有活跃的登录会话"
}
```
### 4.4 失败响应示例(系统异常)
```json
{
"code": 500,
"msg": "获取当前登录会话信息失败: 数据库连接异常"
}
```
## 5. 业务逻辑说明
### 5.1 接口工作原理
1. 接收到请求后,系统会查询数据库中所有状态为"ACTIVE"的登录会话记录
2. 按照登录时间倒序排列,获取最新的会话记录
3. 将会话信息缓存到Redis中以提高下次访问的性能
4. 返回会话详细信息给客户端
### 5.2 会话状态说明
- ACTIVE活跃会话用户当前已登录
- EXPIRED已过期会话用户需要重新登录
- REVOKED已吊销会话用户主动登出或被管理员强制登出
### 5.3 数据来源
会话信息存储在[datai_sf_login_session](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/mapper/DataiSfLoginSessionMapper.java#L17-L17)表中,与[datai_sf_token](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/mapper/DataiSfTokenMapper.java#L17-L17)表通过[tokenId](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/domain/DataiSfLoginSession.java#L101-L101)字段关联。
## 6. 异常处理
### 6.1 常见异常情况
| 异常情况 | 错误码 | 错误信息 | 处理建议 |
|---------|--------|---------|---------|
| 无活跃会话 | 500 | 当前没有活跃的登录会话 | 用户需要重新登录 |
| 数据库异常 | 500 | 获取当前登录会话信息失败: [具体错误信息] | 检查数据库连接和服务状态 |
| 系统内部错误 | 500 | 获取当前登录会话信息失败: [具体错误信息] | 查看系统日志,联系技术支持 |
### 6.2 日志记录
接口调用过程中会产生以下日志:
- INFO级别记录接口调用开始和结束
- DEBUG级别记录详细的处理过程
- ERROR级别记录异常信息
## 7. 性能与安全
### 7.1 性能优化
- 使用Redis缓存会话信息减少数据库访问
- 对数据库查询结果进行排序优化
- 限制返回的会话记录数量(仅返回最新的活跃会话)
### 7.2 安全考虑
- 接口不暴露敏感信息如密码、访问令牌等
- 会话信息仅包含必要的元数据
- 所有操作都记录审计日志
## 8. 相关接口
### 8.1 前置接口
- [/salesforce/login/execute](file:///D:/idea_demo/datai/datai-scenes/datai-scene-salesforce/datai-salesforce-auth/src/main/java/com/datai/auth/controller/DataISfLoginController.java#L48-L74):执行登录,创建会话信息

85
src/api/auth/register.js Normal file
View File

@ -0,0 +1,85 @@
import request from '@/utils/request'
// 执行登录
export function executeLogin(data) {
return request({
url: '/salesforce/login/execute',
method: 'post',
data: data,
headers: {
'Content-Type': 'application/json'
}
})
}
// 刷新访问令牌
export function refreshToken(data) {
return request({
url: '/salesforce/login/refresh-token',
method: 'post',
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
// 执行登出
export function logout(data) {
return request({
url: '/salesforce/login/logout',
method: 'post',
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
// 获取当前登录状态
export function getLoginStatus() {
return request({
url: '/salesforce/login/status',
method: 'get'
})
}
// 验证令牌有效性
export function validateToken(accessToken) {
return request({
url: '/salesforce/login/validate-token',
method: 'get',
params: {
accessToken: accessToken
}
})
}
// 绑定令牌到设备/IP
export function bindToken(data) {
return request({
url: '/salesforce/login/bind-token',
method: 'post',
data: data,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
}
// 检查令牌绑定
export function checkBinding(params) {
return request({
url: '/salesforce/login/check-binding',
method: 'get',
params: params
})
}
// 获取当前登录会话信息
export function getLoginSession() {
return request({
url: '/salesforce/login/session',
method: 'get'
})
}

372
src/views/auth/index.vue Normal file
View File

@ -0,0 +1,372 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { Document, ChatDotRound, User } from '@element-plus/icons-vue';
import { getConfigKey } from '@/api/system/config';
import { useRoute, useRouter } from 'vue-router';
import Oauth from "./oauth.vue";
import { getToken } from '@/utils/auth';
import { RoutesAlias } from '@/router/routesAlias';
//
const pageLoaded = ref(false);
const successCount = ref(0);
const targetSuccessCount = 100;
//
function startCountAnimation() {
const duration = 3000; //
const step = Math.ceil(targetSuccessCount / (duration / 50)); // 50
const interval = setInterval(() => {
successCount.value += step;
if (successCount.value >= targetSuccessCount) {
successCount.value = targetSuccessCount;
clearInterval(interval);
}
}, 50);
}
//
const captchaEnabled = ref(false);
//
const register = ref(false);
const route = useRoute();
console.log(route);
onMounted(async () => {
if (getToken()) {
useRouter().push(RoutesAlias.Home)
return;
}
captchaEnabled.value = await getConfigKey("sys.account.captchaEnabled").then(res => res.msg === 'true')
register.value = await getConfigKey("sys.account.registerUser").then(res => res.msg === 'true')
// true
pageLoaded.value = true;
startCountAnimation();
});
const features = [
{ icon: Document, text: '模块解耦' },
{ icon: ChatDotRound, text: '前沿技术' },
{ icon: User, text: '多端支持' }
];
const method = ref("password");
const methods = computed(() => {
const text = route.name === 'Login' ? '登录' : '注册';
return [
{ label: `密码${text}`, value: "password" },
{ label: `手机${text}`, value: "phone" },
{ label: `邮箱${text}`, value: "email" }
]
});
const title = computed(() => import.meta.env.VITE_APP_TITLE || '后台管理系统');
</script>
<template>
<div class="auth">
<div class="background-animation">
<div v-for="i in 10" :key="i" class="floating-shape"></div>
</div>
<div class="container" :class="{ 'appear-animation': pageLoaded }">
<div class="container-left">
<h1>欢迎使用GEEK生态</h1>
<p>打造高效现代可扩展的企业级开发平台</p>
<div class="features">
<div class="feature-item" v-for="(item, index) in features" :key="index">
<el-icon>
<component :is="item.icon" />
</el-icon>
<span>{{ item.text }}</span>
</div>
</div>
<div class="success-stories">
<div class="story-counter">
<span class="counter">{{ successCount }}+</span>
<span class="counter-label">成功案例</span>
</div>
</div>
</div>
<div class="container-right" v-if="pageLoaded">
<h3 class="title">{{ title }}</h3>
<el-segmented v-model="method" :options="methods" block />
<div class="container-form">
<router-view v-slot="{ Component }">
<component :is="Component" :register="register" :captchaEnabled="captchaEnabled" :method="method" />
</router-view>
</div>
<Oauth />
</div>
</div>
<!-- 底部 -->
<div class="el-auth-footer">
<span>Copyright © 2018-2024 若依Geek后台管理系统 All Rights Reserved.</span>
</div>
</div>
</template>
<style lang='scss' scoped>
@use 'sass:list';
@use 'sass:map';
.auth {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
background: linear-gradient(135deg, var(--el-bg-color) 0%, var(--el-fill-color) 100%);
}
/* 背景动画元素 */
.background-animation {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
.floating-shape {
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--el-color-primary-light-8);
animation: float 15s infinite linear;
@keyframes float {
0% {
transform: translateY(0) scale(1);
opacity: 0.6;
}
50% {
transform: translateY(-40px) scale(1.2);
opacity: 0.3;
}
100% {
transform: translateY(0) scale(1);
opacity: 0.6;
}
}
// 使Sass
$shapes: (
(top: 10%, left: 10%, w: 60px, h: 60px, delay: 0s, duration: 20s),
(top: 20%, right: 20%, w: 100px, h: 100px, delay: 2s, duration: 22s),
(bottom: 15%, left: 15%, w: 70px, h: 70px, delay: 4s, duration: 18s),
(bottom: 25%, right: 10%, w: 50px, h: 50px, delay: 6s, duration: 25s),
(top: 50%, left: 5%, w: 40px, h: 40px, delay: 8s, duration: 30s),
(top: 40%, right: 5%, w: 90px, h: 90px, delay: 10s, duration: 28s),
(top: 70%, left: 30%, w: 55px, h: 55px, delay: 12s, duration: 24s),
(bottom: 40%, right: 25%, w: 65px, h: 65px, delay: 14s, duration: 26s),
(top: 30%, left: 40%, w: 75px, h: 75px, delay: 16s, duration: 29s),
(bottom: 10%, right: 40%, w: 45px, h: 45px, delay: 18s, duration: 27s)
);
@for $i from 1 through list.length($shapes) {
$shape: list.nth($shapes, $i);
&:nth-child(#{$i}) {
@each $key, $value in $shape {
@if $key ==top or $key ==bottom or $key ==left or $key ==right {
#{$key}: #{$value};
}
}
width: map.get($shape, w);
height: map.get($shape, h);
animation-delay: map.get($shape, delay);
animation-duration: map.get($shape, duration);
}
}
}
}
.container {
display: flex;
width: 85%;
@media screen and (max-width: 768px) {
width: 95%;
}
max-width: 1200px;
min-height: 600px;
height: 70vh;
background: var(--el-bg-color);
border-radius: var(--el-border-radius-large);
box-shadow: var(--el-box-shadow);
overflow: hidden;
position: relative;
z-index: 1;
opacity: 0;
transform: translateY(30px);
transition: all 1s ease;
&.appear-animation {
opacity: 1;
transform: translateY(0);
}
}
.container-left {
width: 50%;
background: linear-gradient(135deg, var(--el-color-primary-light-3) 0%, var(--el-color-primary-dark-3) 100%);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: var(--el-color-white);
padding: 40px;
text-align: center;
@media screen and (max-width: 768px) {
display: none;
}
h1 {
font-size: 2.5rem;
margin-bottom: 20px;
font-weight: 600;
text-shadow: 0 2px 4px var(--el-color-primary-dark-2);
}
p {
font-size: 1.1rem;
margin-bottom: 40px;
opacity: 0.9;
line-height: 1.6;
}
.features {
display: flex;
justify-content: space-around;
margin-top: 50px;
width: 80%;
.feature-item {
display: flex;
flex-direction: column;
align-items: center;
transition: all 0.3s ease;
&:hover {
transform: translateY(-8px) scale(1.05);
.el-icon {
box-shadow: 0 8px 16px var(--el-color-primary-dark-2);
transform: rotate(5deg);
}
}
.el-icon {
font-size: 32px;
margin-bottom: 15px;
padding: 8px;
background: var(--el-color-primary-light-8);
border-radius: 50%;
box-shadow: 0 4px 12px var(--el-color-primary-light-5);
transition: all 0.3s ease;
}
span {
font-size: 1rem;
font-weight: 500;
}
}
}
.success-stories {
margin-top: 40px;
.story-counter {
display: flex;
flex-direction: column;
align-items: center;
.counter {
font-size: 3rem;
font-weight: 700;
color: var(--el-color-white);
text-shadow: 0 2px 8px var(--el-color-primary-dark-2);
}
.counter-label {
font-size: 1.2rem;
margin-top: 5px;
font-weight: 500;
}
}
}
}
.container-right {
width: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #fff;
position: relative;
@media screen and (max-width: 768px) {
width: 100%;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at center, rgba(255, 255, 255, 0) 70%, rgba(235, 235, 235, 0.5) 100%);
pointer-events: none;
}
.title {
margin: 30px auto 20px auto;
text-align: center;
color: #333;
font-size: 28px;
font-weight: 600;
background: linear-gradient(45deg, var(--el-color-primary-light-3), var(--el-color-primary-dark-3));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.el-segmented {
background-color: var(--el-color-primary);
padding: 0;
box-shadow: var(--el-box-shadow-light);
:deep(.el-segmented__item) {
border-radius: 0;
background-color: var(--el-color-white);
border-color: var(--el-color-white);
}
}
}
.container-form {
width: 80%;
max-width: 400px;
padding: 20px;
}
.el-auth-footer {
height: 40px;
line-height: 40px;
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
color: #fff;
font-family: Arial;
font-size: 12px;
letter-spacing: 1px;
}
</style>

245
src/views/auth/login.vue Normal file
View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
import { useRouter } from "vue-router";
import { onMounted, ref } from "vue";
import { getCodeImg, sendEmailCode, sendPhoneCode } from "@/api/login";
import { RoutesAlias } from "@/router/routesAlias";
import { useStorage } from "@vueuse/core";
const props = defineProps<{
register: boolean,
captchaEnabled: boolean,
method: 'password' | 'phone' | 'email'
}>()
const userStore = useUserStore()
const router = useRouter();
const loginForm = ref({
username: "",
password: "",
email: '',
phonenumber: '',
code: "",
uuid: "",
rememberMe: false,
autoRegister: false
});
const loginFormState = useStorage('loginForm', loginForm.value);
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
};
const codeUrl = ref("");
const loading = ref(false);
const redirect = ref(undefined);
const loginRef = ref<any | null>(null)
function handleLogin() {
loginRef.value?.validate((valid: any) => {
if (valid) {
loading.value = true;
userStore.login(loginForm.value, props.method).then(() => {
router.push({ path: redirect.value || "/" });
}).catch(() => {
getCode();
}).finally(() => {
loading.value = false;
if (loginForm.value.rememberMe) {
loginFormState.value.username = loginForm.value.username;
loginFormState.value.email = loginForm.value.email;
loginFormState.value.phonenumber = loginForm.value.phonenumber;
loginFormState.value.password = encrypt(loginForm.value.password);
loginFormState.value.rememberMe = loginForm.value.rememberMe;
} else {
loginFormState.value.username = '';
loginFormState.value.password = '';
loginFormState.value.email = '';
loginFormState.value.phonenumber = '';
loginFormState.value.rememberMe = false;
}
});
}
});
}
function getCode() {
if (!props.captchaEnabled) return
getCodeImg().then((res: any) => {
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
});
}
function sendCode() {
if (props.method === 'email') {
sendEmailCode(loginForm.value, 'login')
} else if (props.method === 'phone') {
sendPhoneCode(loginForm.value, 'login')
}
}
function getCookie() {
loginForm.value = {
username: loginFormState.value.username ?? '',
password: loginFormState.value.password ? decrypt(loginFormState.value.password) : '',
email: loginFormState.value.email ?? '',
phonenumber: loginFormState.value.phonenumber ?? '',
code: "",
uuid: "",
rememberMe: !loginFormState.value.rememberMe ? false : loginFormState.value.rememberMe,
autoRegister: false
};
}
onMounted(() => {
getCookie();
getCode();
})
</script>
<template>
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<el-form-item prop="email" v-if="method === 'email'">
<el-input v-model="loginForm.email" type="text" size="large" auto-complete="off" placeholder="邮箱">
<template #prefix>
<svg-icon icon-class="email" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="phonenumber" v-else-if="method === 'phone'">
<el-input v-model="loginForm.phonenumber" type="text" size="large" auto-complete="off" placeholder="手机号">
<template #prefix>
<svg-icon icon-class="phone" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<div v-else>
<el-form-item prop="username">
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
show-password @keyup.enter="handleLogin">
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
</div>
<el-form-item prop="code" v-if="method === 'email' || method === 'phone'">
<el-input size="large" v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleLogin">
<template #prefix>
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
</template>
</el-input>
<div class="login-code">
<el-button class="login-code-img" @click="sendCode">发送验证码</el-button>
</div>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled && method === 'password'">
<el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleLogin">
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
<div style="width:100%;display:flex;justify-content:space-between;align-items:center;">
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住我</el-checkbox>
<el-checkbox v-if="method != 'password'" v-model="loginForm.autoRegister"
style="margin:0px 0px 25px 0px;">若无账号自动注册~</el-checkbox>
</div>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div class="register-link" v-if="register">
<span class="question-text">没有账号</span>
<router-link class="link-type" :to="RoutesAlias.Register">立即注册</router-link>
</div>
</el-form-item>
</el-form>
</template>
<style lang='scss' scoped>
.login-form {
.el-input {
height: 40px;
input {
height: 40px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 0px;
}
}
.login-code {
width: 37%;
height: 40px;
float: right;
.login-code-img {
cursor: pointer;
vertical-align: middle;
width: calc(100% - 12px);
height: 40px;
margin-left: 12px;
}
}
.register-link {
display: flex;
justify-content: center;
align-items: center;
margin-top: 5px;
.question-text {
color: #606266;
margin-right: 8px;
font-size: 14px;
}
.link-type {
color: #409EFF;
text-decoration: none;
font-weight: 500;
font-size: 14px;
position: relative;
transition: all 0.3s ease;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background-color: #409EFF;
transition: width 0.3s ease;
}
&:hover {
color: #66b1ff;
&::after {
width: 100%;
}
}
}
}
</style>

171
src/views/auth/oauth.vue Normal file
View File

@ -0,0 +1,171 @@
<script setup lang="ts">
import { getAction } from '@/utils/request';
function oauthLogin(provider: string) {
console.log(`开始 ${provider} 登录流程`);
// /system/auth
getAction<string>(`/system/auth/login/${provider}`)
.then((response) => {
if (response && response.msg) {
window.open(response.msg, '_blank');
} else {
console.error('登录链接获取失败', response);
}
})
.catch((error) => {
console.error(`登录 ${provider} 失败`, error);
});
}
</script>
<template>
<!-- 第三方登录区域 -->
<div class="other-login">
<div class="divider">
<span class="divider-text">第三方账号登录</span>
</div>
<div class="oauth-buttons">
<div class="oauth-btn" @click="oauthLogin('gitee')" data-tooltip="Gitee登录">
<svg-icon icon-class="gitee" class="oauth-icon gitee" />
<span class="oauth-name">Gitee</span>
</div>
<div class="oauth-btn" @click="oauthLogin('github')" data-tooltip="GitHub登录">
<svg-icon icon-class="github" class="oauth-icon github" />
<span class="oauth-name">GitHub</span>
</div>
<div class="oauth-btn" @click="oauthLogin('qq')" data-tooltip="QQ登录">
<svg-icon icon-class="qq" class="oauth-icon qq" />
<span class="oauth-name">QQ</span>
</div>
<div class="oauth-btn" @click="oauthLogin('wechat')" data-tooltip="微信登录">
<svg-icon icon-class="wechat" class="oauth-icon wechat" />
<span class="oauth-name">微信</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.other-login {
max-width: 360px;
padding: 0 20px;
width: 100%;
.divider {
position: relative;
text-align: center;
margin: 20px 0;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 1px;
background: linear-gradient(90deg, transparent, #eaeaea, transparent);
z-index: 1;
}
.divider-text {
position: relative;
display: inline-block;
padding: 0 12px;
background-color: #fff;
color: #909399;
font-size: 13px;
z-index: 2;
}
}
.oauth-buttons {
display: flex;
justify-content: space-between;
margin-top: 15px;
.oauth-btn {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.5s;
padding: 8px 16px;
border-radius: 4px;
position: relative;
&:hover {
background-color: #f5f5f5;
transform: translateY(-5px);
&::after {
content: attr(data-tooltip);
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 3px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
animation: fadeIn 0.3s forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
}
}
}
.oauth-icon {
font-size: 28px;
margin-bottom: 5px;
transition: transform 0.3s ease;
&.gitee {
color: #c71d23;
}
&.github {
color: #24292e;
}
&.qq {
color: #12b7f5;
}
&.wechat {
color: #07c160;
}
}
.oauth-name {
font-size: 12px;
color: #606266;
position: relative;
transition: all 0.3s ease;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: currentColor;
transition: width 0.3s ease;
}
.oauth-btn:hover & {
font-weight: 500;
&::after {
width: 100%;
}
}
}
}
}
}
</style>

248
src/views/auth/register.vue Normal file
View File

@ -0,0 +1,248 @@
<script setup lang="ts">
import { ElMessageBox } from "element-plus";
import { getCodeImg, sendEmailCode, sendPhoneCode } from "@/api/login";
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import useUserStore from "@/store/modules/user";
import { RoutesAlias } from "@/router/routesAlias";
const router = useRouter();
const userStore = useUserStore()
const props = defineProps<{
register: boolean,
captchaEnabled: boolean,
method: 'password' | 'phone' | 'email'
}>()
const registerForm = ref({
username: "",
password: "",
confirmPassword: "",
email: '',
phonenumber: '',
code: "",
uuid: "",
});
const equalToPassword = (rule: any, value: any, callback: Function) => {
if (registerForm.value.password !== value) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
};
const registerRules = {
username: [
{ required: true, trigger: "blur", message: "请输入您的账号" },
{ min: 2, max: 20, message: "用户账号长度必须介于 2 和 20 之间", trigger: "blur" }
],
password: [
{ required: true, trigger: "blur", message: "请输入您的密码" },
{ min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
confirmPassword: [
{ required: true, trigger: "blur", message: "请再次输入您的密码" },
{ required: true, validator: equalToPassword, trigger: "blur" }
],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
};
const codeUrl = ref("");
const loading = ref(false);
const registerRef = ref<any | null>(null)
function handleRegister() {
registerRef.value?.validate((valid: any) => {
if (valid) {
loading.value = true;
userStore.register(registerForm.value, props.method).then(() => {
const username = registerForm.value.username;
ElMessageBox.alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", "系统提示", {
dangerouslyUseHTMLString: true,
type: "success",
}).then(() => {
router.push(RoutesAlias.Login);
}).catch(() => { });
}).catch(() => {
getCode();
}).finally(() => {
loading.value = false;
});
}
});
}
function getCode() {
if (!props.captchaEnabled) return
getCodeImg().then((res: any) => {
codeUrl.value = "data:image/gif;base64," + res.img;
registerForm.value.uuid = res.uuid;
});
}
function sendCode() {
if (props.method === 'email') {
sendEmailCode(registerForm.value, 'register')
} else if (props.method === 'phone') {
sendPhoneCode(registerForm.value, 'register')
}
}
onMounted(() => {
getCode();
})
</script>
<template>
<el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
<el-form-item prop="email" v-if="method === 'email'">
<el-input v-model="registerForm.email" type="text" size="large" auto-complete="off" placeholder="邮箱">
<template #prefix>
<svg-icon icon-class="email" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="phonenumber" v-else-if="method === 'phone'">
<el-input v-model="registerForm.phonenumber" type="text" size="large" auto-complete="off" placeholder="手机号">
<template #prefix>
<svg-icon icon-class="phone" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="username" v-else>
<el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" placeholder="账号">
<template #prefix>
<svg-icon icon-class="user" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="registerForm.password" type="password" size="large" auto-complete="off" placeholder="密码"
@keyup.enter="handleRegister">
<template #prefix>
<svg-icon icon-class="password" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" size="large" auto-complete="off"
placeholder="确认密码" @keyup.enter="handleRegister">
<template #prefix>
<svg-icon icon-class="password" class="el-input__icon input-icon" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="method === 'email' || method === 'phone'">
<el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleRegister">
<template #prefix>
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
</template>
</el-input>
<div class="register-code">
<el-button class="register-code-img" @click="sendCode">发送验证码</el-button>
</div>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled && method === 'password'">
<el-input size="large" v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleRegister">
<template #prefix>
<svg-icon icon-class="validCode" class="el-input__icon input-icon" />
</template>
</el-input>
<div class="register-code">
<img :src="codeUrl" @click="getCode" class="register-code-img" />
</div>
</el-form-item>
<el-form-item style="width:100%;">
<el-button :loading="loading" size="large" type="primary" style="width:100%;" @click.prevent="handleRegister">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div class="login-link">
<span class="question-text">已有账号</span>
<router-link class="link-type" :to="RoutesAlias.Login">立即登录</router-link>
</div>
</el-form-item>
</el-form>
</template>
<style lang='scss' scoped>
.register-form {
.el-input {
height: 40px;
input {
height: 40px;
}
}
.input-icon {
height: 39px;
width: 14px;
margin-left: 0px;
}
}
.register-code {
width: 37%;
height: 40px;
float: right;
.register-code-img {
cursor: pointer;
vertical-align: middle;
width: calc(100% - 12px);
height: 40px;
margin-left: 12px;
}
}
.login-link {
display: flex;
justify-content: center;
align-items: center;
margin-top: 5px;
.question-text {
color: #606266;
margin-right: 8px;
font-size: 14px;
}
.link-type {
color: #409EFF;
text-decoration: none;
font-weight: 500;
font-size: 14px;
position: relative;
transition: all 0.3s ease;
&::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background-color: #409EFF;
transition: width 0.3s ease;
}
&:hover {
color: #66b1ff;
&::after {
width: 100%;
}
}
}
}
</style>

View File

@ -0,0 +1,741 @@
<template>
<div class="salesforce-login-container">
<div class="login-header">
<h2>Salesforce 认证中心</h2>
<p class="login-description">选择适合您的登录方式连接到 Salesforce</p>
</div>
<!-- 登录表单 -->
<div class="login-section" v-if="!isLoggedIn">
<el-card class="login-card" shadow="hover">
<el-tabs v-model="activeTab" @tab-change="handleTabChange" class="login-tabs">
<!-- OAuth 2.0 登录 -->
<el-tab-pane label="OAuth 2.0" name="oauth2">
<div class="tab-content">
<div class="tab-description">
<h3>OAuth 2.0 认证</h3>
<p>使用标准的 OAuth 2.0 协议进行安全认证</p>
</div>
<el-form
ref="oauth2FormRef"
:model="oauth2Form"
:rules="oauth2Rules"
label-width="120px"
class="login-form"
>
<el-form-item label="授权类型" prop="grantType">
<el-select v-model="oauth2Form.grantType" placeholder="请选择授权类型" style="width: 100%">
<el-option label="密码模式" value="password"></el-option>
<el-option label="客户端凭证模式" value="client_credentials"></el-option>
<el-option label="授权码模式" value="authorization_code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="客户端ID" prop="clientId">
<el-input v-model="oauth2Form.clientId" placeholder="请输入客户端ID"></el-input>
</el-form-item>
<el-form-item label="客户端密钥" prop="clientSecret">
<el-input v-model="oauth2Form.clientSecret" type="password" placeholder="请输入客户端密钥" show-password></el-input>
</el-form-item>
<el-form-item label="用户名" prop="username" v-show="oauth2Form.grantType === 'password'">
<el-input v-model="oauth2Form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" v-show="oauth2Form.grantType === 'password'">
<el-input v-model="oauth2Form.password" type="password" placeholder="请输入密码" show-password></el-input>
</el-form-item>
<el-form-item label="安全令牌" prop="securityToken" v-show="oauth2Form.grantType === 'password'">
<el-input v-model="oauth2Form.securityToken" placeholder="请输入安全令牌(可选)"></el-input>
</el-form-item>
<el-form-item label="授权码" prop="code" v-show="oauth2Form.grantType === 'authorization_code'">
<el-input v-model="oauth2Form.code" placeholder="请输入授权码"></el-input>
</el-form-item>
<el-form-item label="State" prop="state" v-show="oauth2Form.grantType === 'authorization_code'">
<el-input v-model="oauth2Form.state" placeholder="请输入state参数"></el-input>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="handleOAuth2Login" :loading="loading" size="large">
<el-icon><Connection /></el-icon>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- 传统凭据登录 -->
<el-tab-pane label="传统凭据" name="legacy_credential">
<div class="tab-content">
<div class="tab-description">
<h3>传统凭据认证</h3>
<p>使用用户名和密码进行传统登录</p>
</div>
<el-form
ref="legacyFormRef"
:model="legacyForm"
:rules="legacyRules"
label-width="120px"
class="login-form"
>
<el-form-item label="用户名" prop="username">
<el-input v-model="legacyForm.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="legacyForm.password" type="password" placeholder="请输入密码" show-password></el-input>
</el-form-item>
<el-form-item label="安全令牌" prop="securityToken">
<el-input v-model="legacyForm.securityToken" placeholder="请输入安全令牌(可选)"></el-input>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="handleLegacyLogin" :loading="loading" size="large">
<el-icon><User /></el-icon>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- Session ID 登录 -->
<el-tab-pane label="Session ID" name="session_id">
<div class="tab-content">
<div class="tab-description">
<h3>Session ID 认证</h3>
<p>使用现有的 Session ID 直接登录</p>
</div>
<el-form
ref="sessionIdFormRef"
:model="sessionIdForm"
:rules="sessionIdRules"
label-width="120px"
class="login-form"
>
<el-form-item label="Session ID" prop="sessionId">
<el-input v-model="sessionIdForm.sessionId" placeholder="请输入Session ID" type="textarea" :rows="3"></el-input>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="handleSessionIdLogin" :loading="loading" size="large">
<el-icon><Key /></el-icon>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- Salesforce CLI 登录 -->
<el-tab-pane label="Salesforce CLI" name="salesforce_cli">
<div class="tab-content">
<div class="tab-description">
<h3>Salesforce CLI 认证</h3>
<p>使用已配置的 Salesforce CLI 凭据登录</p>
</div>
<el-form
ref="cliFormRef"
:model="cliForm"
label-width="120px"
class="login-form"
>
<el-form-item label="组织别名" prop="orgAlias">
<el-input v-model="cliForm.orgAlias" placeholder="请输入组织别名(可选)"></el-input>
</el-form-item>
<el-form-item class="form-actions">
<el-button type="primary" @click="handleCliLogin" :loading="loading" size="large">
<el-icon><Monitor /></el-icon>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
<!-- 登录成功后的会话信息展示 -->
<div v-if="isLoggedIn && sessionInfo" class="session-info-section">
<el-card class="session-card" shadow="hover">
<template #header>
<div class="card-header">
<h3>会话信息</h3>
<el-tag :type="sessionInfo.status === 'active' ? 'success' : 'danger'" size="large">
{{ sessionInfo.status === 'active' ? '活跃' : '非活跃' }}
</el-tag>
</div>
</template>
<el-descriptions :column="2" border class="session-descriptions">
<el-descriptions-item label="会话ID" span="2">
<el-text class="session-id" copyable>{{ sessionInfo.sessionId }}</el-text>
</el-descriptions-item>
<el-descriptions-item label="租户编号">{{ sessionInfo.tenantId }}</el-descriptions-item>
<el-descriptions-item label="用户名">{{ sessionInfo.username }}</el-descriptions-item>
<el-descriptions-item label="登录类型">{{ sessionInfo.loginType }}</el-descriptions-item>
<el-descriptions-item label="会话状态">
<el-tag :type="sessionInfo.status === 'active' ? 'success' : 'danger'">
{{ sessionInfo.status }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="登录时间">{{ sessionInfo.loginTime }}</el-descriptions-item>
<el-descriptions-item label="过期时间">{{ sessionInfo.expireTime }}</el-descriptions-item>
<el-descriptions-item label="最后活动时间">{{ sessionInfo.lastActivityTime }}</el-descriptions-item>
<el-descriptions-item label="登录IP" span="2">{{ sessionInfo.loginIp }}</el-descriptions-item>
<el-descriptions-item label="设备信息" span="2">{{ sessionInfo.deviceInfo }}</el-descriptions-item>
<el-descriptions-item label="浏览器信息" span="2">{{ sessionInfo.browserInfo }}</el-descriptions-item>
<el-descriptions-item label="令牌ID" span="2">
<el-text class="token-id" copyable>{{ sessionInfo.tokenId }}</el-text>
</el-descriptions-item>
</el-descriptions>
<div class="session-actions">
<el-button type="primary" @click="fetchSessionInfo" :loading="loading">
<el-icon><Refresh /></el-icon>
刷新会话信息
</el-button>
<el-button type="danger" @click="logout">
<el-icon><SwitchButton /></el-icon>
退出登录
</el-button>
</div>
</el-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { executeLogin as salesforceLogin, getLoginSession as getCurrentSession } from '@/api/auth/register'
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
import { Connection, User, Key, Monitor, Refresh, SwitchButton } from '@element-plus/icons-vue'
//
const oauth2FormRef = ref<FormInstance>()
const legacyFormRef = ref<FormInstance>()
const sessionIdFormRef = ref<FormInstance>()
const cliFormRef = ref<FormInstance>()
//
const activeTab = ref('oauth2')
//
const loading = ref(false)
//
const isLoggedIn = ref(false)
//
const sessionInfo = ref<any>(null)
// OAuth2
const oauth2Form = reactive({
loginType: 'oauth2',
grantType: 'password',
username: '',
password: '',
securityToken: '',
clientId: '',
clientSecret: '',
code: '',
state: ''
})
//
const legacyForm = reactive({
loginType: 'legacy_credential',
username: '',
password: '',
securityToken: ''
})
// Session ID
const sessionIdForm = reactive({
loginType: 'session_id',
sessionId: ''
})
// CLI
const cliForm = reactive({
loginType: 'salesforce_cli',
orgAlias: ''
})
// OAuth2
const oauth2Rules = reactive<FormRules>({
grantType: [{ required: true, message: '请选择授权类型', trigger: 'change' }],
clientId: [{ required: true, message: '请输入客户端ID', trigger: 'blur' }],
clientSecret: [{ required: true, message: '请输入客户端密钥', trigger: 'blur' }],
username: [{
required: false,
message: '请输入用户名',
trigger: 'blur',
validator: (rule, value, callback) => {
if (oauth2Form.grantType === 'password' && !value) {
callback(new Error('密码模式下用户名为必填项'))
} else {
callback()
}
}
}],
password: [{
required: false,
message: '请输入密码',
trigger: 'blur',
validator: (rule, value, callback) => {
if (oauth2Form.grantType === 'password' && !value) {
callback(new Error('密码模式下密码为必填项'))
} else {
callback()
}
}
}],
code: [{
required: false,
message: '请输入授权码',
trigger: 'blur',
validator: (rule, value, callback) => {
if (oauth2Form.grantType === 'authorization_code' && !value) {
callback(new Error('授权码模式下授权码为必填项'))
} else {
callback()
}
}
}],
state: [{
required: false,
message: '请输入state参数',
trigger: 'blur',
validator: (rule, value, callback) => {
if (oauth2Form.grantType === 'authorization_code' && !value) {
callback(new Error('授权码模式下state参数为必填项'))
} else {
callback()
}
}
}]
})
//
const legacyRules = reactive<FormRules>({
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
})
// Session ID
const sessionIdRules = reactive<FormRules>({
sessionId: [{ required: true, message: '请输入Session ID', trigger: 'blur' }]
})
//
const handleTabChange = () => {
//
oauth2FormRef.value?.clearValidate()
legacyFormRef.value?.clearValidate()
sessionIdFormRef.value?.clearValidate()
cliFormRef.value?.clearValidate()
}
//
const handleLogin = async (formRef: FormInstance | undefined, formData: any, loginType: string) => {
if (!formRef) return
await formRef.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// CLI
let loginParams = { ...formData }
if (loginType === 'salesforce_cli' && !loginParams.orgAlias) {
delete loginParams.orgAlias
}
await salesforceLogin(loginParams)
ElMessage.success('登录成功')
await fetchSessionInfo()
} catch (error: any) {
console.error(`${loginType}登录失败:`, error)
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的凭据'
ElMessage.error(errorMessage)
} finally {
loading.value = false
}
}
})
}
// OAuth2
const handleOAuth2Login = async () => {
if (!oauth2FormRef.value) return
await oauth2FormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true
try {
// - OAuth2ID
const loginParams: any = {
loginType: oauth2Form.loginType,
grantType: oauth2Form.grantType,
clientId: oauth2Form.clientId,
clientSecret: oauth2Form.clientSecret
}
//
if (oauth2Form.grantType === 'password') {
loginParams.username = oauth2Form.username
loginParams.password = oauth2Form.password
//
if (oauth2Form.securityToken) {
loginParams.securityToken = oauth2Form.securityToken
}
} else if (oauth2Form.grantType === 'authorization_code') {
loginParams.code = oauth2Form.code
loginParams.state = oauth2Form.state
}
// client_credentials clientId clientSecret
await salesforceLogin(loginParams)
ElMessage.success('登录成功')
await fetchSessionInfo()
} catch (error: any) {
console.error('OAuth2登录失败:', error)
const errorMessage = error.response?.data?.message || error.message || '登录失败,请检查您的凭据'
ElMessage.error(errorMessage)
} finally {
loading.value = false
}
}
})
}
//
const handleLegacyLogin = () => handleLogin(legacyFormRef.value, legacyForm, '传统凭据')
// Session ID
const handleSessionIdLogin = () => handleLogin(sessionIdFormRef.value, sessionIdForm, 'Session ID')
// CLI
const handleCliLogin = () => {
if (!cliFormRef.value) return
loading.value = true
try {
//
const loginParams = { ...cliForm }
if (!loginParams.orgAlias) {
delete loginParams.orgAlias
}
salesforceLogin(loginParams).then(() => {
ElMessage.success('登录成功')
fetchSessionInfo()
}).catch((error: any) => {
console.error('CLI登录失败:', error)
const errorMessage = error.response?.data?.message || error.message || '登录失败请检查您的CLI配置'
ElMessage.error(errorMessage)
}).finally(() => {
loading.value = false
})
} catch (error: any) {
console.error('CLI登录失败:', error)
const errorMessage = error.response?.data?.message || error.message || '登录失败请检查您的CLI配置'
ElMessage.error(errorMessage)
loading.value = false
}
}
//
const fetchSessionInfo = async () => {
try {
const response = await getCurrentSession()
if (response.code === 200) {
sessionInfo.value = response.data
isLoggedIn.value = true
} else {
ElMessage.error(response.msg || '获取会话信息失败')
isLoggedIn.value = false
sessionInfo.value = null
}
} catch (error: any) {
console.error('获取会话信息失败:', error)
const errorMessage = error.response?.data?.message || error.message || '获取会话信息失败'
ElMessage.error(errorMessage)
isLoggedIn.value = false
sessionInfo.value = null
}
}
//
onMounted(() => {
fetchSessionInfo()
})
// 退
const logout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
isLoggedIn.value = false
sessionInfo.value = null
ElMessage.success('已退出登录')
}).catch(() => {
// 退
})
}
</script>
<style scoped lang="scss">
.salesforce-login-container {
padding: 24px;
max-width: 900px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
.login-header {
text-align: center;
margin-bottom: 32px;
h2 {
font-size: 28px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
.login-description {
font-size: 16px;
color: #606266;
max-width: 600px;
margin: 0 auto;
}
}
.login-section {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.login-card {
border-radius: 12px;
overflow: hidden;
.login-tabs {
:deep(.el-tabs__header) {
margin-bottom: 24px;
}
:deep(.el-tabs__nav-wrap) {
&::after {
height: 1px;
}
}
:deep(.el-tabs__item) {
font-size: 16px;
font-weight: 500;
padding: 0 24px;
}
}
.tab-content {
padding: 0 8px;
.tab-description {
margin-bottom: 24px;
text-align: center;
h3 {
font-size: 20px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
p {
font-size: 14px;
color: #909399;
}
}
.login-form {
max-width: 500px;
margin: 0 auto;
:deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-input__wrapper) {
border-radius: 6px;
}
:deep(.el-select .el-input .el-input__wrapper) {
border-radius: 6px;
}
.form-actions {
margin-top: 32px;
text-align: center;
.el-button {
padding: 12px 32px;
font-size: 16px;
border-radius: 6px;
.el-icon {
margin-right: 8px;
}
}
}
}
}
}
}
.session-info-section {
margin-top: 32px;
.session-card {
border-radius: 12px;
overflow: hidden;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
font-size: 20px;
font-weight: 600;
color: #303133;
margin: 0;
}
}
.session-descriptions {
margin: 24px 0;
:deep(.el-descriptions__label) {
font-weight: 500;
color: #606266;
}
:deep(.el-descriptions__content) {
color: #303133;
}
.session-id, .token-id {
font-family: monospace;
font-size: 13px;
word-break: break-all;
}
}
.session-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 24px;
.el-button {
padding: 10px 24px;
border-radius: 6px;
.el-icon {
margin-right: 8px;
}
}
}
}
}
}
@media (max-width: 768px) {
.salesforce-login-container {
padding: 16px;
.login-header {
h2 {
font-size: 24px;
}
.login-description {
font-size: 14px;
}
}
.login-section {
.login-card {
.login-tabs {
:deep(.el-tabs__item) {
font-size: 14px;
padding: 0 16px;
}
}
.tab-content {
.tab-description {
h3 {
font-size: 18px;
}
p {
font-size: 13px;
}
}
.login-form {
max-width: 100%;
.form-actions {
.el-button {
padding: 10px 24px;
font-size: 14px;
}
}
}
}
}
}
.session-info-section {
.session-card {
.session-descriptions {
:deep(.el-descriptions__label) {
font-size: 13px;
}
:deep(.el-descriptions__content) {
font-size: 13px;
}
}
.session-actions {
flex-direction: column;
align-items: center;
.el-button {
width: 100%;
max-width: 200px;
}
}
}
}
}
}
</style>