头图
写在最前面,该方案是三方应用免登设计方案。去除了涵盖公司机密信息部分,阅读起来只能给大家一个思路参考~

流程说明

对接钉钉、飞书、企业微信及自研三方平台实现H5免登录,不同平台协议的核心差异在授权流程,下面详细对钉钉侧说明。

钉钉(企业内部应用授权协议)

  • 临时凭证时效性:通过dd.getAuthCode获取的授权码(code)仅5分钟有效,且需后端在失效前完成access_token和用户信息的获取。
  • 两步Token交换:需先通过AppKey+AppSecret换取access_token,再结合code获取用户userid,需严格匹配安全可信域名,否则触发domain is not secure错误。

交互逻辑展示

image-20250331094153386

技术设计思路

根据上述需求,主要划分为两类数据:动态配置项固定属性

  • 动态配置项主要包括:字段映射、平台差异配置
  • 固定属性主要包括:标题、类型、状态

考虑到后续需要适配国产数据库,原计划采用结构化表JSON混合存储。国产数据库多基于PostgreSQL(如GaussDB)或自研内核(如达梦),其SQL语法、存储引擎设计与MySQL存在本质差异。将JSON部分采用LONGTEXT类型。

为什么放弃采用VARCHAR类型存储?

  1. 部分国产数据库(如达梦DM8)的VARCHAR最大长度为32767,但若迁移工具未自动转换类型,可能保留VARCHAR(2000)定义,导致超长数据报错。
  2. 为适配不同平台不同树深度JSON内容,本身需要动态配置JSONPath规则,不依赖与数据库层JSON能力。
  3. MySQL支持天然支持LONGTEXT类型,且数据存储后一般不会二次查询使用。

数据库表结构设计方案

平台配置表(kb_sso_platform_config):存储平台基础信息

CREATE TABLE kb_sso_platform_config
(
    id                int auto_increment comment '主键id,自增长列'
        primary key,
    type_plat_form      varchar(64)              not null comment '平台类型(如钉钉)',
    type              varchar(32)              not null comment '协议类型(OAuth2.0、AD等)',
    address_the_Request text COMMENT '请求地址(JSON格式存储步骤)',
    tenant_id         varchar(50)              not null comment '租户id',
    creator           varchar(100) default '0' not null comment '创建人',
    create_time       datetime                 not null comment '创建时间',
    modifier          varchar(100) default '0' null comment '修改人',
    modifier_time     datetime                 not null comment '修改时间',
    sort              int          default 0   not null comment '排序',
    deprecate_tag     int          default 0   not null comment '废弃标签'
);

说明:针对不同平台请求url不同使用json的方式存储在addressTheRequest 中,下面以钉钉为例。

{
  "accessTokenUrl": {
    "url": "https://oapi.dingtalk.com/gettoken?appkey=dingvppdnkxxxxovs2&appsecret=2H2YVMHrttAzHe60tjLgOOjvu-V0n_V8OY_46WcKwCUG-ZDAMxxxxe_6e"
  },
  "userIdInfoUrl": {
    "url": "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo?access_token=ed17535a7dxxxf90de6027c"
  },
  "userDeilUrl": {
    "url": "https://oapi.dingtalk.com/topapi/v2/user/get?access_token=ed17535a7d04xxxx90de6027c"
  }
}

技术方案设计

映射字段对应

不管采用何种端进行免登。需要进行区分的只有平台类型会影响到解析调用三方(钉钉、飞书、微信等)获取到的JSON格式数据。所以采用yaml语法存储JSONPath表达式进行不同平台的字段映射;下面是示例:

rules:
  dingtalk:  # 新增钉钉平台配置
    unique_field: "$.result.mobile"  # 主路径
    backup_paths: ["$.mobile"]      # 备用路径(防止数据结构变化)
  wechat:
    unique_field: "$.data.UserId"
    backup_paths: ["$.userid", "$.response.user_id"]
  alipay: 
    unique_field: "$.result.contact_info.phone"

如何判断终端类型

数据库中不存储终端类型信息,后端返回时候提供,下面是代码示例:

public ResponseEntity<?> type(HttpServletRequest request) {
    String userAgent = request.getHeader("User-Agent").toLowerCase();
    boolean isMobile = userAgent.matches(".*(mobile|android|iphone).*");
    
    return isMobile ? 
        redirectTo("/h5-auth") : 
        redirectTo("/pc-auth");
}

代码大致逻辑

总体校验流程

public interface AuthHandler {
    String getAccessToken(String code) ;
    String getUserId(String token) ;
    UserDetail getUserDetail(String userId) ;
}

抽象工厂

public abstract class AuthHandlerFactory<T extends AuthHandler> {
    private final Class<T> handlerClass;

    protected AuthHandlerFactory(Class<T> handlerClass) {
        this.handlerClass = handlerClass;
    }

    // 支持反射创建(需无参构造)
    public T createHandler() {
        try {
            return handlerClass.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException("处理器实例化失败", e);
        }
    }
}

注册表管理类

public class AuthHandlerRegistry {
    private static final Map<String, AuthHandlerFactory<?>> registry = new ConcurrentHashMap<>();

    // 预注册已知平台(钉钉)
    static {
        register("dingtalk", new DingTalkAuthFactory());
    }

    // 动态注册方法
    public static void register(String platform, AuthHandlerFactory<?> factory) {
        registry.put(platform.toLowerCase(), factory);
    }

    // 获取处理器工厂
    public static AuthHandlerFactory<?> getFactory(String platform) {
        return Optional.ofNullable(registry.get(platform.toLowerCase()))
                .orElseThrow(() -> new IllegalArgumentException("未注册的平台: " + platform));
    }

    // 创建处理器实例
    public static AuthHandler createHandler(String platform) {
        return getFactory(platform).createHandler();
    }
}

具体实例化工厂

public class DingTalkAuthFactory extends AuthHandlerFactory<DingTalkAuthHandler> {
    public DingTalkAuthFactory() {
        super(DingTalkAuthHandler.class);
    }
}

具体调用示例

public static void main(String[] args) {
    // 获取钉钉处理器
    AuthHandler dingtalkHandler = AuthHandlerRegistry.createHandler("dingtalk");

    // 获取飞书处理器
    AuthHandler feishuHandler = AuthHandlerRegistry.createHandler("feishu");

    // 业务逻辑

}

舒一笑不秃头
37 声望95 粉丝

生成式AI应用工程师(高级)认证