datai/datai-scenes/datai-scene-salesforce/docs/design/2026-01-29-001-01-认证和会话管理-设计.md

25 KiB
Raw Permalink Blame History

设计文档

元数据

  • 需求编号001
  • 子需求编号001-01
  • 创建时间2026-01-29
  • 创建人AI Assistant
  • 状态:已完成

设计概述

本设计文档基于 Spring Boot 和若依框架,实现 Salesforce Partner API 的认证和会话管理功能。核心设计包括:

  1. 工厂模式实现:通过 PartnerConnectionFactory 工厂类创建和管理 PartnerConnection 连接,继承 AbstractConnectionFactory 抽象类
  2. 连接缓存机制:利用 AbstractConnectionFactory 提供的连接缓存和自动 Session 有效性检查功能
  3. 会话管理集成:集成 SessionManager 获取会话信息,支持 Session 过期时自动重新登录
  4. RESTful API 设计:提供标准的 REST API 接口,包括获取会话信息、获取用户信息、修改密码等功能
  5. 异常处理机制:使用 datai-salesforce-common 模块的异常体系,统一处理各种异常情况

架构设计

系统架构图

graph TB
    subgraph "Controller 层"
        A[PartnerConnectionController]
    end

    subgraph "Service 层"
        B[PartnerConnectionService]
    end

    subgraph "Factory 层"
        C[PartnerConnectionFactory]
    end

    subgraph "Auth 模块"
        D[SessionManager]
        E[AbstractConnectionFactory]
    end

    subgraph "Common 模块"
        F[异常体系]
    end

    subgraph "Salesforce API"
        G[PartnerConnection]
        H[Connector]
    end

    A --> B
    B --> C
    C --> D
    C --> E
    B --> F
    C --> H
    H --> G

模块架构图

datai-salesforce-partner/
├── controller/
│   └── PartnerConnectionController.java
├── service/
│   ├── IPartnerConnectionService.java
│   └── impl/
│       └── PartnerConnectionServiceImpl.java
├── factory/
│   └── PartnerConnectionFactory.java
└── model/
    ├── dto/
    │   ├── SessionInfoDto.java
    │   ├── UserInfoDto.java
    │   └── ChangePasswordDto.java
    └── vo/
        ├── SessionInfoVo.java
        ├── UserInfoVo.java
        └── ChangePasswordVo.java

数据流图

sequenceDiagram
    participant Client as 客户端
    participant Controller as Controller
    participant Service as Service
    participant Factory as Factory
    participant SessionManager as SessionManager
    participant Salesforce as Salesforce API

    Client->>Controller: GET /api/partner/session
    Controller->>Service: getSessionInfo()
    Service->>SessionManager: getCurrentSession("source")
    SessionManager-->>Service: sessionId
    Service->>SessionManager: getInstanceUrl("source")
    SessionManager-->>Service: instanceUrl
    Service-->>Controller: SessionInfo
    Controller-->>Client: JSON Response

    Client->>Controller: GET /api/partner/user-info
    Controller->>Service: getUserInfo()
    Service->>Factory: getConnection("source")
    Factory->>SessionManager: getCurrentSession("source")
    Factory->>SessionManager: getInstanceUrl("source")
    Factory->>Salesforce: createConnection()
    Salesforce-->>Factory: PartnerConnection
    Factory-->>Service: PartnerConnection
    Service->>PartnerConnection: getUserInfo()
    PartnerConnection-->>Service: GetUserInfoResult
    Service-->>Controller: UserInfo
    Controller-->>Client: JSON Response

    Client->>Controller: POST /api/partner/change-password
    Controller->>Service: changePassword(oldPassword, newPassword)
    Service->>Factory: getConnection("source")
    Factory-->>Service: PartnerConnection
    Service->>PartnerConnection: changeOwnPassword()
    PartnerConnection-->>Service: ChangeOwnPasswordResult
    Service-->>Controller: Result
    Controller-->>Client: JSON Response

技术方案

技术选型

技术组件 版本/类型 选择理由
Spring Boot 3.x 成熟稳定,生态丰富,支持自动配置
Spring Web 3.x 提供 RESTful API 支持
Spring Security 6.x 提供权限控制和认证
MyBatis Plus 3.x 简化数据库操作,提供 CRUD 功能
Salesforce Partner API WSC 58.x 官方 Java 客户端,功能完整
Lombok 1.18.x 简化 Java 代码,减少样板代码
Swagger/OpenAPI 3.x 自动生成 API 文档
若依框架 最新版 提供系统基础功能,如用户管理、权限管理

核心算法设计

连接创建算法

protected PartnerConnection createConnection(String orgType) {
    String sessionId = sessionManager.getCurrentSession(orgType);
    String instanceUrl = sessionManager.getInstanceUrl(orgType);
    
    ConnectorConfig config = new ConnectorConfig();
    config.setSessionId(sessionId);
    config.setRestEndpoint(instanceUrl + "/services/Soap/u/" + API_VERSION);
    config.setCompression(true);
    config.setTraceMessage(false);
    
    return Connector.newConnection(config);
}

复杂度分析

  • 时间复杂度O(1) - 直接创建连接对象
  • 空间复杂度O(1) - 只创建一个连接对象

优化点

  • 使用连接缓存避免重复创建
  • 自动检查 Session 有效性
  • 支持多线程安全访问

Session 有效性检查算法

public boolean isSessionValid(String orgType) {
    SalesforceLoginResult loginResult = getCurrentLoginResult(orgType);
    return loginResult != null 
            && loginResult.isSuccess() 
            && !loginResult.isSessionExpired();
}

复杂度分析

  • 时间复杂度O(1) - 直接检查登录结果
  • 空间复杂度O(1) - 不需要额外空间

优化点

  • 使用缓存存储登录结果
  • 支持自动重新登录
  • 异常处理完善

集成方案设计

与 SessionManager 集成

集成方式

  • 通过依赖注入获取 SessionManager 实例
  • 调用 getCurrentSession("source") 获取 Session ID
  • 调用 getInstanceUrl("source") 获取实例 URL
  • 自动处理 Session 过期情况

数据交换格式

  • Session IDString 类型
  • 实例 URLString 类型
  • 登录结果SalesforceLoginResult 对象

错误处理机制

  • 捕获 SessionManager 抛出的异常
  • 转换为 SalesforceAuthException
  • 返回友好的错误消息

重试机制

  • SessionManager 内部已实现自动重新登录
  • 不需要在 Service 层额外实现重试
  • 最多重试 3 次(由 SessionManager 控制)

与 Salesforce Partner API 集成

集成方式

  • 使用 WSC (Web Services Connector) 库
  • 通过 Connector.newConnection() 创建连接
  • 调用 PartnerConnection 的方法执行操作

数据交换格式

  • 请求SOAP/XML 格式
  • 响应SOAP/XML 格式

错误处理机制

  • 捕获 ConnectionException
  • 捕获 ApiFault
  • 转换为自定义异常

重试机制

  • 使用 SalesforceConstants.MAX_RETRY_COUNT 控制重试次数
  • 使用 SalesforceConstants.RETRY_DELAY_MS 控制重试延迟

数据模型

数据库表设计

本需求不涉及数据库表的创建或修改,所有数据均来自 Salesforce API。

实体类设计

SessionInfoDto

package com.datai.partner.model.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
@Schema(description = "会话信息 DTO")
public class SessionInfoDto {
    
    @Schema(description = "会话 ID")
    private String sessionId;
    
    @Schema(description = "实例 URL")
    private String instanceUrl;
    
    @Schema(description = "是否成功")
    private Boolean success;
}

UserInfoDto

package com.datai.partner.model.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
@Schema(description = "用户信息 DTO")
public class UserInfoDto {
    
    @Schema(description = "用户 ID")
    private String userId;
    
    @Schema(description = "用户名")
    private String userName;
    
    @Schema(description = "用户全名")
    private String userFullName;
    
    @Schema(description = "用户邮箱")
    private String userEmail;
    
    @Schema(description = "组织 ID")
    private String organizationId;
    
    @Schema(description = "组织名称")
    private String organizationName;
    
    @Schema(description = "用户语言")
    private String userLanguage;
    
    @Schema(description = "用户时区")
    private String userTimeZone;
    
    @Schema(description = "货币符号")
    private String currencySymbol;
}

ChangePasswordDto

package com.datai.partner.model.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
@Schema(description = "修改密码 DTO")
public class ChangePasswordDto {
    
    @NotBlank(message = "旧密码不能为空")
    @Schema(description = "旧密码")
    private String oldPassword;
    
    @NotBlank(message = "新密码不能为空")
    @Schema(description = "新密码")
    private String newPassword;
}

SessionInfoVo

package com.datai.partner.model.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
@Schema(description = "会话信息 VO")
public class SessionInfoVo {
    
    @Schema(description = "会话 ID")
    private String sessionId;
    
    @Schema(description = "实例 URL")
    private String instanceUrl;
}

UserInfoVo

package com.datai.partner.model.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Data
@Schema(description = "用户信息 VO")
public class UserInfoVo {
    
    @Schema(description = "用户 ID")
    private String userId;
    
    @Schema(description = "用户名")
    private String userName;
    
    @Schema(description = "用户全名")
    private String userFullName;
    
    @Schema(description = "用户邮箱")
    private String userEmail;
    
    @Schema(description = "组织 ID")
    private String organizationId;
    
    @Schema(description = "组织名称")
    private String organizationName;
    
    @Schema(description = "用户语言")
    private String userLanguage;
    
    @Schema(description = "用户时区")
    private String userTimeZone;
    
    @Schema(description = "货币符号")
    private String currencySymbol;
}

数据字典设计

本需求不涉及数据字典的创建或修改。

接口设计

RESTful API 设计

1. 获取会话信息

接口路径GET /api/partner/session

接口描述:获取当前 Salesforce 会话信息

请求参数:无

响应格式

{
  "code": 200,
  "msg": "查询成功",
  "data": {
    "sessionId": "00D...!AQEAQ...",
    "instanceUrl": "https://your-instance.salesforce.com"
  }
}

错误响应

{
  "code": 500,
  "msg": "获取会话信息失败: 没有有效的会话信息",
  "data": null
}

2. 获取用户信息

接口路径GET /api/partner/user-info

接口描述:获取当前 Salesforce 用户信息

请求参数:无

响应格式

{
  "code": 200,
  "msg": "查询成功",
  "data": {
    "userId": "005...",
    "userName": "user@example.com",
    "userFullName": "John Doe",
    "userEmail": "user@example.com",
    "organizationId": "00D...",
    "organizationName": "Acme Corp",
    "userLanguage": "en_US",
    "userTimeZone": "America/Los_Angeles",
    "currencySymbol": "$"
  }
}

错误响应

{
  "code": 500,
  "msg": "获取用户信息失败: Session 无效",
  "data": null
}

3. 修改密码

接口路径POST /api/partner/change-password

接口描述:修改当前用户密码

请求参数

{
  "oldPassword": "OldPassword123",
  "newPassword": "NewPassword456"
}

响应格式

{
  "code": 200,
  "msg": "密码修改成功",
  "data": null
}

错误响应

{
  "code": 500,
  "msg": "密码修改失败: 旧密码不正确",
  "data": null
}

接口权限设计

所有接口都需要登录认证,但不需要特定权限。

@PreAuthorize("@ss.hasLogin()")

接口文档设计

Swagger 注解示例

@Tag(name = "【Partner API】认证和会话管理")
@RestController
@RequestMapping("/api/partner")
public class PartnerConnectionController extends BaseController {

    @Operation(summary = "获取会话信息")
    @PreAuthorize("@ss.hasLogin()")
    @GetMapping("/session")
    public AjaxResult getSessionInfo() {
        // 实现
    }

    @Operation(summary = "获取用户信息")
    @PreAuthorize("@ss.hasLogin()")
    @GetMapping("/user-info")
    public AjaxResult getUserInfo() {
        // 实现
    }

    @Operation(summary = "修改密码")
    @PreAuthorize("@ss.hasLogin()")
    @PostMapping("/change-password")
    public AjaxResult changePassword(@Valid @RequestBody ChangePasswordDto dto) {
        // 实现
    }
}

实现要点

关键实现逻辑

1. 工厂层实现

PartnerConnectionFactory 类

package com.datai.partner.factory;

import com.datai.auth.core.SessionManager;
import com.datai.auth.factory.AbstractConnectionFactory;
import com.datai.common.constant.SalesforceConstants;
import com.sforce.soap.partner.Connector;
import com.sforce.soap.partner.ConnectorConfig;
import com.sforce.soap.partner.PartnerConnection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class PartnerConnectionFactory extends AbstractConnectionFactory<PartnerConnection> {

    private static final String API_VERSION = "v58.0";

    @Override
    protected PartnerConnection createConnection(String orgType) {
        try {
            String sessionId = sessionManager.getCurrentSession(orgType);
            String instanceUrl = sessionManager.getInstanceUrl(orgType);
            
            ConnectorConfig config = new ConnectorConfig();
            config.setSessionId(sessionId);
            config.setRestEndpoint(instanceUrl + "/services/Soap/u/" + API_VERSION);
            config.setCompression(true);
            config.setTraceMessage(false);
            
            PartnerConnection connection = Connector.newConnection(config);
            
            log.info("PartnerConnection 创建成功ORG类型: {}", orgType);
            return connection;
            
        } catch (Exception e) {
            log.error("创建 PartnerConnection 失败ORG类型: {}, 错误: {}", orgType, e.getMessage(), e);
            throw new RuntimeException("创建 PartnerConnection 失败: " + e.getMessage(), e);
        }
    }
}

实现要点

  • 继承 AbstractConnectionFactory利用其提供的连接缓存和 Session 有效性检查功能
  • 通过 SessionManager 获取 Session ID 和实例 URL
  • 创建 ConnectorConfig 并设置必要的配置
  • 使用 Connector.newConnection() 创建 PartnerConnection 实例
  • 异常处理完善,记录详细日志

2. Service 层实现

IPartnerConnectionService 接口

package com.datai.partner.service;

import com.datai.partner.model.dto.ChangePasswordDto;
import com.datai.partner.model.dto.UserInfoDto;
import com.sforce.soap.partner.GetUserInfoResult;

public interface IPartnerConnectionService {
    
    String getSessionId();
    
    String getInstanceUrl();
    
    GetUserInfoResult getUserInfo();
    
    void changePassword(String oldPassword, String newPassword);
}

PartnerConnectionServiceImpl 实现类

package com.datai.partner.service.impl;

import com.datai.auth.core.SessionManager;
import com.datai.common.exception.SalesforceAuthException;
import com.datai.common.exception.SalesforceLoginException;
import com.datai.partner.factory.PartnerConnectionFactory;
import com.datai.partner.service.IPartnerConnectionService;
import com.sforce.soap.partner.GetUserInfoResult;
import com.sforce.soap.partner.PartnerConnection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class PartnerConnectionServiceImpl implements IPartnerConnectionService {

    @Autowired
    private PartnerConnectionFactory partnerConnectionFactory;

    @Autowired
    private SessionManager sessionManager;

    @Override
    public String getSessionId() {
        try {
            return sessionManager.getCurrentSession("source");
        } catch (Exception e) {
            log.error("获取 Session ID 失败: {}", e.getMessage(), e);
            throw new SalesforceAuthException("获取 Session ID 失败: " + e.getMessage());
        }
    }

    @Override
    public String getInstanceUrl() {
        try {
            return sessionManager.getInstanceUrl("source");
        } catch (Exception e) {
            log.error("获取实例 URL 失败: {}", e.getMessage(), e);
            throw new SalesforceAuthException("获取实例 URL 失败: " + e.getMessage());
        }
    }

    @Override
    public GetUserInfoResult getUserInfo() {
        try {
            PartnerConnection connection = partnerConnectionFactory.getConnection("source");
            return connection.getUserInfo();
        } catch (Exception e) {
            log.error("获取用户信息失败: {}", e.getMessage(), e);
            throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
        }
    }

    @Override
    public void changePassword(String oldPassword, String newPassword) {
        try {
            PartnerConnection connection = partnerConnectionFactory.getConnection("source");
            connection.changeOwnPassword(oldPassword, newPassword);
            log.info("密码修改成功");
        } catch (Exception e) {
            log.error("修改密码失败: {}", e.getMessage(), e);
            throw new SalesforceAuthException("修改密码失败: " + e.getMessage());
        }
    }
}

实现要点

  • 使用 @Autowired 注入 PartnerConnectionFactory 和 SessionManager
  • 所有方法固定使用 "source" orgType
  • 通过工厂获取 PartnerConnection 实例
  • 调用 PartnerConnection 的方法执行操作
  • 异常处理完善,使用 datai-salesforce-common 的异常类

3. Controller 层实现

PartnerConnectionController 类

package com.datai.partner.controller;

import com.datai.common.core.controller.BaseController;
import com.datai.common.core.domain.AjaxResult;
import com.datai.partner.model.dto.ChangePasswordDto;
import com.datai.partner.model.dto.UserInfoDto;
import com.datai.partner.service.IPartnerConnectionService;
import com.sforce.soap.partner.GetUserInfoResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@Slf4j
@Tag(name = "【Partner API】认证和会话管理")
@RestController
@RequestMapping("/api/partner")
public class PartnerConnectionController extends BaseController {

    @Autowired
    private IPartnerConnectionService partnerConnectionService;

    @Operation(summary = "获取会话信息")
    @PreAuthorize("@ss.hasLogin()")
    @GetMapping("/session")
    public AjaxResult getSessionInfo() {
        try {
            String sessionId = partnerConnectionService.getSessionId();
            String instanceUrl = partnerConnectionService.getInstanceUrl();
            
            return success(sessionId, instanceUrl);
        } catch (Exception e) {
            log.error("获取会话信息失败: {}", e.getMessage(), e);
            return error("获取会话信息失败: " + e.getMessage());
        }
    }

    @Operation(summary = "获取用户信息")
    @PreAuthorize("@ss.hasLogin()")
    @GetMapping("/user-info")
    public AjaxResult getUserInfo() {
        try {
            GetUserInfoResult result = partnerConnectionService.getUserInfo();
            
            UserInfoDto dto = new UserInfoDto();
            dto.setUserId(result.getUserId());
            dto.setUserName(result.getUserName());
            dto.setUserFullName(result.getUserFullName());
            dto.setUserEmail(result.getUserEmail());
            dto.setOrganizationId(result.getOrganizationId());
            dto.setOrganizationName(result.getOrganizationName());
            dto.setUserLanguage(result.getUserLanguage());
            dto.setUserTimeZone(result.getUserTimeZone());
            dto.setCurrencySymbol(result.getCurrencySymbol());
            
            return success(dto);
        } catch (Exception e) {
            log.error("获取用户信息失败: {}", e.getMessage(), e);
            return error("获取用户信息失败: " + e.getMessage());
        }
    }

    @Operation(summary = "修改密码")
    @PreAuthorize("@ss.hasLogin()")
    @PostMapping("/change-password")
    public AjaxResult changePassword(@Valid @RequestBody ChangePasswordDto dto) {
        try {
            partnerConnectionService.changePassword(dto.getOldPassword(), dto.getNewPassword());
            return success("密码修改成功");
        } catch (Exception e) {
            log.error("修改密码失败: {}", e.getMessage(), e);
            return error("修改密码失败: " + e.getMessage());
        }
    }
}

实现要点

  • 继承 BaseController使用其提供的 success() 和 error() 方法
  • 使用 @PreAuthorize 注解进行权限控制
  • 使用 @Valid 注解进行参数验证
  • 使用 @Tag 和 @Operation 注解生成 Swagger 文档
  • 异常处理完善,返回友好的错误消息

异常处理设计

可能出现的异常

  1. SalesforceAuthException:认证异常

    • 原因Session 无效、Session 过期、获取会话信息失败
    • 处理:记录日志,返回友好的错误消息
  2. SalesforceLoginException:登录异常

    • 原因:登录失败、用户名或密码错误
    • 处理:记录日志,返回友好的错误消息
  3. ConnectionException:连接异常

    • 原因网络问题、Salesforce API 不可用
    • 处理:记录日志,返回友好的错误消息
  4. ApiFaultAPI 错误

    • 原因Salesforce API 返回错误
    • 处理:记录日志,返回友好的错误消息

异常处理方式

try {
    PartnerConnection connection = partnerConnectionFactory.getConnection("source");
    GetUserInfoResult result = connection.getUserInfo();
    return success(result);
} catch (ConnectionException e) {
    log.error("获取用户信息失败: {}", e.getMessage(), e);
    throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
} catch (ApiFault e) {
    log.error("获取用户信息失败: {}", e.getMessage(), e);
    throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
} catch (Exception e) {
    log.error("获取用户信息失败: {}", e.getMessage(), e);
    throw new SalesforceAuthException("获取用户信息失败: " + e.getMessage());
}

异常提示信息

  • "获取会话信息失败: 没有有效的会话信息"
  • "获取用户信息失败: Session 无效"
  • "修改密码失败: 旧密码不正确"
  • "创建 PartnerConnection 失败: 网络连接失败"

性能优化设计

1. 连接缓存

优化策略

  • 使用 AbstractConnectionFactory 提供的连接缓存
  • 避免重复创建连接,提高性能
  • 自动检查 Session 有效性,过期时自动清除缓存

性能目标

  • 连接创建时间:< 100ms
  • 缓存命中率:> 90%
  • 并发支持:> 100 TPS

2. 多线程安全

优化策略

  • 使用 ReentrantLock 保证多线程环境下的连接获取安全
  • 使用 ConcurrentHashMap 存储连接缓存
  • 避免竞态条件和死锁

性能目标

  • 并发访问:支持 > 100 个并发请求
  • 响应时间:< 50ms缓存命中

3. 压缩传输

优化策略

  • 启用 ConnectorConfig.setCompression(true)
  • 减少 SOAP 消息传输大小
  • 提高网络传输效率

性能目标

  • 传输数据量减少:> 50%
  • 网络传输时间减少:> 30%

安全设计

1. 数据加密

加密方式

  • 使用 HTTPS 协议传输数据
  • Session ID 通过加密通道传输
  • 密码修改时使用加密传输

2. 权限控制

控制方式

  • 使用 @PreAuthorize 注解进行权限控制
  • 所有接口都需要登录认证
  • 不需要特定权限,但需要登录

3. 防注入方式

防 SQL 注入

  • 不直接拼接 SQL 语句
  • 使用参数化查询

防 XSS 攻击

  • 对用户输入进行验证和过滤
  • 使用 @Valid 注解进行参数验证
  • 不直接输出用户输入的内容

相关文档