如何实现H5端对接钉钉登录并优雅扩展其他平台
- 钉钉H5登录逻辑
- 后端代码如何实现?
- 本次采用策略模式+工厂方式进行
- 定义接口确定会使用的基本鉴权步骤
- 具体逻辑类进行实现
- 采用注册表模式(Registry Pattern)
- 抽象工厂进行基本逻辑定义
- 具体工厂进行对接口中的逻辑步骤具体----实例化----逻辑进行重写
- 总结
钉钉H5登录逻辑
下图中需要说明的一点是,准确来说步骤3来说是钉钉API返回给前端,前端携带一次性校验码token给后端进行后续的鉴权。
还有一点需要注意获得权限之后,如果前端需要回调接口获取用户信息,则需要增加上下文中的用户信息存储
后端代码如何实现?
具体的伪代码如下所述,下面细聊一下如何进行实现获取用户信息这一步。其中本次采用了设计模式进行实现。
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));// 校验通过下发tokenString accessToken = StpUtil.getTokenInfo().getTokenValue();xxxxreturn Result.success(loginResult);}
本次我的思路是实现针对不同平台,例如对接钉钉、企业微信、飞书、三方,具体的逻辑是不一样的,使用设计模式中的工厂模式进行构建,实现不同的逻辑进行创建不同类进行完成。
简单罗列一下可以采用的设计模式的具体之间的区别
本次采用策略模式+工厂方式进行
定义接口确定会使用的基本鉴权步骤
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;}@Overridepublic 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();}@Overridepublic 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();}@Overridepublic 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;@Overridepublic 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;}@Overridepublic 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