写在最前面,该方案是三方应用免登设计方案。去除了涵盖公司机密信息部分,阅读起来只能给大家一个思路参考~
流程说明
对接钉钉、飞书、企业微信及自研三方平台实现H5免登录,不同平台协议的核心差异在授权流程,下面详细对钉钉侧说明。
钉钉(企业内部应用授权协议)
- 临时凭证时效性:通过
dd.getAuthCode
获取的授权码(code)仅5分钟有效,且需后端在失效前完成access_token
和用户信息的获取。 - 两步Token交换:需先通过
AppKey
+AppSecret
换取access_token
,再结合code
获取用户userid
,需严格匹配安全可信域名,否则触发domain is not secure
错误。
交互逻辑展示
技术设计思路
根据上述需求,主要划分为两类数据:动态配置项和固定属性。
- 动态配置项主要包括:字段映射、平台差异配置
- 固定属性主要包括:标题、类型、状态
考虑到后续需要适配国产数据库,原计划采用结构化表与JSON混合存储。国产数据库多基于PostgreSQL(如GaussDB)或自研内核(如达梦),其SQL语法、存储引擎设计与MySQL存在本质差异。将JSON部分采用LONGTEXT类型。
为什么放弃采用VARCHAR类型存储?
- 部分国产数据库(如达梦DM8)的
VARCHAR
最大长度为32767,但若迁移工具未自动转换类型,可能保留VARCHAR(2000)
定义,导致超长数据报错。- 为适配不同平台不同树深度JSON内容,本身需要动态配置JSONPath规则,不依赖与数据库层JSON能力。
- 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");
// 业务逻辑
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。