如何實現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