如何实现H5端对接钉钉登录并优雅扩展其他平台

发布于:2025-04-11 ⋅ 阅读:(44) ⋅ 点赞:(0)

钉钉H5登录逻辑

下图中需要说明的一点是,准确来说步骤3来说是钉钉API返回给前端,前端携带一次性校验码token给后端进行后续的鉴权。
还有一点需要注意获得权限之后,如果前端需要回调接口获取用户信息,则需要增加上下文中的用户信息存储

image-20250407212849539

后端代码如何实现?

具体的伪代码如下所述,下面细聊一下如何进行实现获取用户信息这一步。其中本次采用了设计模式进行实现。

    public Result<LoginResp> h5Login(LoginH5UserReq loginH5UserReq) throws ApiException {
        // 获取租户信息
        xxxxx
        // 查询三方鉴权配置信息
        xxxxx
        // 获取用户信息 这一步很关键后面细说如何实现
        H5AuthHandler H5AuthHandler = H5AuthHandlerRegistry.createHandler(loginH5UserReq.getTypePlatForm());
        String userUniqueIdentifier = H5AuthHandler.getUserDetail(loginH5UserReq);
        // 系统校验根据手机号查询用户信息
        SysUser sysUser = sysUserMapper.selectOne(Wrappers.lambdaQuery(SysUser.class).eq(SysUser::getTel, userUniqueIdentifier), false);
        // 使用断言进行优雅校验
        Assert.notNull(sysUser, () -> new BizException(ErrorCodeEnum.NOT_AVAILABLE));
        // 校验通过下发token
        String accessToken = StpUtil.getTokenInfo().getTokenValue();
       	xxxx
        return Result.success(loginResult);
    }

本次我的思路是实现针对不同平台,例如对接钉钉、企业微信、飞书、三方,具体的逻辑是不一样的,使用设计模式中的工厂模式进行构建,实现不同的逻辑进行创建不同类进行完成。

简单罗列一下可以采用的设计模式的具体之间的区别

image-20250407214413483

本次采用策略模式+工厂方式进行

定义接口确定会使用的基本鉴权步骤

public interface AuthHandler {
    // 获取访问令牌(需处理OAuth2 code校验)
    String getAccessToken(String code) throws AuthException;
    
    // 使用令牌换取用户唯一标识(需处理令牌失效场景)
    String getUserId(String token) throws AuthException;
    
    // 获取用户详细信息(需处理多层级JSON解析)
    UserDetail getUserDetail(String userId) throws AuthException;
}

具体逻辑类进行实现

下面代码是大致思路展示,直接run是会出现问题。涉及公司保密协议不可以直接上我的源码望读者朋友见谅~

public class DingTalkAuthHandler implements AuthHandler {
    private static final String API_HOST = "https://oapi.dingtalk.com";
    private final String appKey;
    private final String appSecret;

    // 依赖配置注入(参考网页6的钉钉配置)
    public DingTalkAuthHandler(String appKey, String appSecret) {
        this.appKey = appKey;
        this.appSecret = appSecret;
    }

    @Override
    public String getAccessToken(String code) {
        // 构建认证请求参数(参考网页7的code交换逻辑)
        Map<String, String> params = new HashMap<>();
        params.put("appkey", appKey);
        params.put("appsecret", appSecret);
        
        // 调用钉钉API(网页6的接口文档)
        String url = API_HOST + "/gettoken?" + buildQueryString(params);
        JsonNode response = HttpUtil.get(url);
        
        // 错误码校验(参考网页6的errcode处理)
        if(response.get("errcode").asInt() != 0) {
            throw new DingTalkAuthException(response.get("errmsg").asText());
        }
        return response.get("access_token").asText();
    }

    @Override
    public String getUserId(String token) {
        // 安全域名验证(参考网页7的domain校验)
        String url = API_HOST + "/user/getuserinfo?access_token=" + token;
        JsonNode userInfo = HttpUtil.get(url);
        return userInfo.get("userid").asText();
    }

    @Override
    public UserDetail getUserDetail(String userId) {
        // 多层级数据解析(参考网页6的JSON结构)
        String url = API_HOST + "/user/get?userid=" + userId;
        JsonNode data = HttpUtil.get(url).get("result");
        
        return UserDetail.builder()
            .mobile(data.at("/mobile").asText()) // JSONPath定位
            .name(data.get("name").asText())
            .avatar(data.get("avatar").asText())
            .build();
    }
    
    // 私有方法封装请求构建
    private String buildQueryString(Map<String, String> params) {
        return params.entrySet().stream()
            .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
            .collect(Collectors.joining("&"));
    }
}

上述代码通过接口 + 实现类的方式进行大致逻辑的定义,具体逻辑的展开,不是本次的重点,主要想记录一下如何实现下述的调用:

// 需要实现根据loginH5UserReq.getTypePlatForm() 传入不同的类型,实现实例化对应的实体类进行处理对应逻辑
H5AuthHandler H5AuthHandler = H5AuthHandlerRegistry.createHandler(loginH5UserReq.getTypePlatForm());
// 得到具体逻辑类之后根据请求信息返回用户唯一的id进行后续鉴权
String userUniqueIdentifier = H5AuthHandler.getUserDetail(loginH5UserReq);

采用注册表模式(Registry Pattern)

集中管理平台与工厂映射关系,提供统一访问入口

@Component
// 为什么要采用ApplicationContextAware?文末解释
public class H5AuthHandlerRegistry implements ApplicationContextAware {
    
    private static final Map<String, H5AuthHandlerFactory<?>> REGISTRY = new ConcurrentHashMap<>();
    private static ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext context) {
        applicationContext = context;
        // Spring容器初始化完成后动态注册平台
        registerPlatforms();
    }
    
    // 具体平台注册
    private void registerPlatforms() {
        // 钉钉平台注册(依赖注入已生效)
        H5DingTalkAuthFactory dingTalkFactory = new H5DingTalkAuthFactory(applicationContext);
        REGISTRY.put(Platforms.DING_TALK.name(), dingTalkFactory);
        // 其他平台注册
        xxxxxxx
    }
    
    // 获取处理器工厂
    public static H5AuthHandlerFactory<?> getFactory(String platform) {
        return Optional.ofNullable(REGISTRY.get(platform))
                .orElseThrow(() -> new IllegalArgumentException("未注册的平台: " + platform));
    }
    
    // 全局同意访问入口
    public static H5AuthHandler createHandler(String platform) {
        return getFactory(platform).createHandler();
    }
}

抽象工厂进行基本逻辑定义

为什么这里要使用抽象类?

首先我想定义基本的创建逻辑,其次抽象类不能被实例化。还有抽象类一般用于设计模式中一种通用写法规范,为子类提供公共的代码实现(如非抽象方法)和强制约束(如抽象方法),子类继承并实现所有抽象方法后才能实例化。

public abstract class H5AuthHandlerFactory<T extends H5AuthHandler> {
    
    private final Class<T> handlerClass;
    
    protected H5AuthHandlerFactory(Class<T> handlerClass) {
        this.handlerClass = handlerClass;
    }
    
    // 定义基本创建逻辑,采用反射方式进行。支持反射创建(需无参构造)
    // PS:如果具体进行逻辑类不涉及采用spring容器管理类,可以使用直接newInstance。不然会出现创建失败,spring容器ioc和Java创建对象是割裂的两派
    public T createHandler() {
        try {
            return handlerClass.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException("H5端登录逻辑抽象工厂---H5AuthHandlerFactory---处理器实例化失败", e);
        }
    }
    
}

具体工厂进行对接口中的逻辑步骤具体----实例化----逻辑进行重写

public class H5DingTalkAuthFactory extends H5AuthHandlerFactory<H5DingTalkAuthHandler> {
    
    private final ApplicationContext context;
    
    // 这里是因为具体实例化处理钉钉H5登录逻辑类会使用到spring容器中的类,所以需要采用上下文的方式
    public H5DingTalkAuthFactory(ApplicationContext context) {
        super(H5DingTalkAuthHandler.class);
        this.context = context;
    }
    
    @Override
    public H5DingTalkAuthHandler createHandler() {
        // 从Spring容器获取依赖项
        ThreePartyLoginRuleConfig ruleConfig = context.getBean(ThreePartyLoginRuleConfig.class);
        ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
        // 通过构造器注入依赖
        return new H5DingTalkAuthHandler(ruleConfig, objectMapper);
    }
}

总结

总体来说,要实现其他平台的扩展。本次的使用中,由于对接不同平台,具体逻辑中涉及了配置文件配置不同平台JSON数据的解析,所以会使用sping中IOC功能,所以在工厂类中存在上下文部分。

扩展其他平台部分就需要创建两个类,一个类是集成抽象工厂实现其中的createHandler()方法,还有一个是实现接口中定义的三部曲。

H5xxxxxxxAuthFactory extends H5AuthHandlerFactory
H5xxxxxxxxAuthHandler implements H5AuthHandler

网站公告

今日签到

点亮在社区的每一天
去签到