如何实现H5端对接钉钉登录并优雅扩展其他平台
钉钉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));
// 校验通过下发token
String accessToken = StpUtil.getTokenInfo().getTokenValue();
xxxx
return 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;
}
@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