登录打通SDK设计

"如何设计SDK"

Posted by wanglilong on October 25, 2019

登录打通SDK设计

JDCAuthorizationSDK包含了日志、弹窗、网络请求、web、接口、京东授权六大部分。以完成京东授权登录打通的工作

一、日志系统

日志系统使用的是裁剪后的CocoaLumberjack框架。

原CocoaLumberjack框架类图如下。

DDAbstractLogger的类簇,是管理日志输出到哪里。分别是 Xcode console,MAC的日志系统,文件和数据库。

DDLogFormatter的类簇,是管理输出格式的。有多线程,黑白名单,和多格式输出。

我们只使用了其部分功能。所以对其进行裁剪。

1、只用到 Xcode console 输入。所以DDFileLogger、DDAbstractDatabaseLogger、DDASLogger类全部删除,只保留输出到 Xcode console 的DDTTYLogger。

2、输出格式DDLogFormatter的子类,全部删除。改为使用自定义格式。

3、为防止发生冲突,所有的类,全部改为JDC开头。

自定义JDCNormalLogFormatter如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (NSString *)formatLogMessage:(JDCLogMessage *)logMessage{
    NSString *functionName = [NSString stringWithCString:logMessage.function.UTF8String encoding:NSASCIIStringEncoding];
    
    NSString *flagName = @"";
    if (logMessage.flag&JDCLogFlagError) {
        flagName = @"Error";
    }else if (logMessage.flag&JDCLogFlagWarning){
        flagName = @"Warning";
    }else if (logMessage.flag&JDCLogFlagInfo){
        flagName = @"Info";
    }else if (logMessage.flag&JDCLogFlagDebug){
        flagName = @"Debug";
    }else if (logMessage.flag&JDCLogFlagVerbose){
        flagName = @"Verbose";
    }
    
    return [NSString stringWithFormat:@"%@ [JDCAuthorizationSDK] [%@]:%@\n end:{fun:%@ ,line:%lu}",logMessage.timestamp,flagName,logMessage.message,functionName, (unsigned long)logMessage.line];
}

二、弹窗系统

弹窗系统使用JKCategories中的一个类(UIView+JKToast)。对其进行重命名为UIView+JDCToast。

并在JDCityToast类中进行简单封装。

1、对具体实现进行简单隔离,方便框架的隔离。

2、使其在对顶端view上弹出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (UIView *)getView
{
    UIWindow *topView = [UIApplication sharedApplication].keyWindow;
    for (UIWindow *win in [[UIApplication sharedApplication].windows  reverseObjectEnumerator]) {
        if ([win isEqual: topView]) {
            continue;
        }
        if (win.windowLevel > topView.windowLevel && win.hidden != YES ) {
            topView =win;
        }
    }
    return topView;
}

三、网络请求

由于框架内仅包含一个网络请求。为了减少依赖,未选用第三方网络请求框架。而是使用系统NSURLRequest类发起网络请求。

仅仅对http头部信息进行简单封装。另外其包含一个MD5加密的简单工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+(NSMutableURLRequest *)makeURLRequest:(NSString *)urlString secretKey:(NSString *)secretKey appCode:(NSString*) appCode modelCode:(NSString *)modelCode{
    NSString *packageName = [NSString stringWithFormat:@"%@%@",[JDCNetwork getBundleID],secretKey];
    NSString *packageNameMD5 = [packageName md5HashToLower32Bit];

    NSURL *url = [NSURL URLWithString:urlString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue: @"application/json" forHTTPHeaderField:@"Content-Type"];
    [request setValue: @"ios" forHTTPHeaderField:@"clientType"];
    [request setValue: appCode forHTTPHeaderField:@"appCode"];
    [request setValue: packageNameMD5 forHTTPHeaderField:@"packageName"];
    
    request.HTTPBody = [NSJSONSerialization dataWithJSONObject:@{@"modelCode":modelCode} options:NSJSONWritingPrettyPrinted error:nil];
    request.HTTPMethod = @"POST";
    return request;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+(void)requestAuthWithSecretKey:(NSString *)secretKey
                        appCode:(NSString*) appCode
                      modelCode:(NSString *)modelCode
                        success:(nullable void (^)(NSDictionary *responseDic,NSString *urlString,NSString *jdAppId))success
                        failure:(nullable void (^)(NSError *error))failure

{
    NSURLSessionConfiguration * configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    NSURLSession *sharedSession = [NSURLSession sessionWithConfiguration:configuration];
    NSURLRequest *request = [self makeURLRequest:JDCAuthURL secretKey:secretKey appCode:appCode modelCode:modelCode];
    
    NSURLSessionDataTask *dataTask = [sharedSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    }];
    [dataTask resume];
}

四、Web管理系统

在JDCURLJumpManager类中对其进行隔离,对外只暴露方法

1
2
3
4
5
6
/**
 普通跳转
 
 @param url 要跳转的url
 */
+ (void)jumpWithUrl:(NSString *)url;

Webview的类使用了,洪亮的BaseViewController和BaseWKWebViewController。其主要对进度条,导航栏进行了管理。

我主要对其进行重命名,并继承后使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pragma mark - ICUBaseVCProtocol
- (BOOL)icu_baseVCNavigationBarHidden {
    return NO;
}

- (BOOL)icu_baseVCNavigationBarBottomLineHidden {
    return YES;
}

- (BOOL)icu_baseVCNavigationBarTranslucent {
    return NO;
}

- (UIStatusBarStyle)preferredStatusBarStyle {
    return UIStatusBarStyleDefault;
}

- (UIImage *)icu_imageForCloseButton {
    return [UIImage jdc_imageNamed:@"main_navi_webView_close_icon"];
}

- (UIImage *)icu_imageForBackButton {
    if ([self.wkWebView canGoBack]) {
        return [UIImage jdc_imageNamed:@"main_navi_webView_back_icon"];
    }else {
        return [UIImage jdc_imageNamed:@"main_navi_back_icon"];
    }
}

- (BOOL)icu_shouldResetNavigationBarState {
    return YES;
}

包内图片获取使用了一个小工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (UIImage *)jdc_imageNamed:(NSString *)name {
    NSBundle *bundle = [self pickerBundle];
    JDCLogInfo(@"bundle %@",bundle);
    return [UIImage imageNamed:name
                      inBundle:bundle compatibleWithTraitCollection:nil];
}

+ (NSBundle *)pickerBundle{
    return [self icr_bundleForClass:NSClassFromString(@"JDCAuth") name:@"JDCAuthorizationSource"];
    
}
+ (NSBundle *)icr_bundleForClass:(Class)class name:(NSString *)bundleName{
    // Get the top level "bundle" which may actually be the framework
    NSBundle *mainBundle = [NSBundle bundleForClass:class];
    
    // Check to see if the resource bundle exists inside the top level bundle
    NSBundle *resourcesBundle = [NSBundle bundleWithPath:[mainBundle pathForResource:bundleName ofType:@"bundle"]];
    
    if (resourcesBundle == nil) {
        resourcesBundle = mainBundle;
    }
    return resourcesBundle;
}

五、接口暴露

为了不暴露实际实现,我们只暴露了协议JDCAuthProtocol。实际的方法调用,通过JDCAuth类代理给JDCAuthManager。代理通过消息发送机制的消息转发实现。

1
2
3
4
5
6
7
8
9
10
+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    return [NSClassFromString(@"JDCAuthManager") class];
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return [NSClassFromString(@"JDCAuthManager") sharedManager];
}

1、接口的设计

1、只需要一个管理类管理京 东登录,所以使用单例生成实例。

2、不希望用户关闭日志系统和Toast系统,分别通过单独接口来开关。

3、appScheme、secretKey 、appCode 只需要设置一次,故通过一个函数来控制。

4、每次启动App可能需要不同的modelCode,所以启动模块时同时设置modelCode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@protocol JDCAuthProtocol <NSObject>

#pragma mark - 配置
/**
 返回实例方法
 
 @return 遵循JDCAuthHeader协议的实例
 */
+ (id<JDCAuthProtocol>)sharedManager;

/**
 是否开启日志系统(默认开启,只debug模式下生效)
 
 @param isLog 是否开启
 */
- (void)setIsLog:(BOOL)isLog;

/**
 是否开启Toast(默认开启)
 
 @param isToast 是否开启
 */
- (void)setIsToast:(BOOL)isToast;

/**
 配置信息
 
 @param appScheme app的Scheme
 @param secretKey 分配的秘钥
 @param appCode app的code
 */
- (void)setAppScheme:(nonnull NSString *)appScheme andSecretKey:(nonnull NSString *)secretKey andAppCode:(nonnull NSString *)appCode;

#pragma mark - 启动&退出

/***
 启动模块(请先配置信息)
 
 @param modelCode 模块的code
 */
- (void)jdUnionLoginWithModelCode:(NSString *)modelCode;

/**
 * 退出登录
 */
- (void)logout;

@end

2、错误正确返回

错误返回通过block进行抛出。但是由于京东SDK的错误分为两类,一种是只有NSError,一种是errorMessage+replyCode。所以返回为NSError、errorMessage和replyCode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 成功block
 @param tokenString token字符串
 @param url url在webview中直接调用(如果使用自定义webview)
 */
typedef void (^JDCAuthSuccessBlock) (NSString * _Nullable tokenString,NSString * _Nonnull url);

/**
 错误block
 @param errorMessage 错误消息(一般为服务器返回。存在时,error为空)
 @param replyCode 错误码 (一般为服务器返回。0)
 @param error 错误消息(与errorMessage互斥)
 @param errorType 错误类型
 */
typedef void (^JDCAuthFailedBlock) (NSString  * __nullable errorMessage,NSUInteger replyCode,NSError * __nullable error,JDCAuthErrorType errorType);

另外还包含一些自己的错误,所以添加JDCAuthErrorType errorType进行区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
typedef enum JDCAuthErrorType {
    JDCAuthErrorNullData = 100001,                         // 有空数据
    JDCAuthErrorGetURLFailed = 100002,                     // 获取连接失败

    JDCAuthErrorAuthorizeFailed = 200001,                  // 登录失败,服务器返回错误
    JDCAuthErrorAuthorizeError = 200002,                   // 登录失败,网络访问错误
    JDCAuthErrorAuthorizeCancle = 200003,                  // 放弃登录
    
    JDCAuthErrorValidateSigntureFailed = 300001,           // 验证失败,服务器返回错误
    JDCAuthErrorValidateSigntureError = 300002,            // 验证失败,网络访问错误

    JDCAuthErrorJumpTokenFailed = 400001,                  // APP 跳转 H5 失败
    JDCAuthErrorJumpTokenError = 400002,                   // APP 跳转 H5 出错
    JDCAuthErrorJumpTokenNoURL = 400003,                   // 没有要跳转的url

    JDCAuthErrorH5BackToAppFailed = 500001,                // 从h5页跳回 APP 失败
    JDCAuthErrorH5BackToAppError = 500002,                 // 从h5页跳回 APP 出错

    JDCAuthErrorAccountLocked = 600001,                    // 账号被锁定,需要去找回密码
    JDCAuthErrorRiskWithBindedPhone = 600002,              // 风险用户登录
    JDCAuthErrorLimitTime = 600003,                        // 账号因为安全策略被禁止登录
    JDCAuthErrorVerificationFailed = 600004,               // 登录失败(也包括刷新验证码失败、刷新票据失败等)
    JDCAuthErrorVerificationError = 600005,                // 登录出错


} JDCAuthErrorType;

同时通过👇方法来添加block,获取返回。

1
2
3
4
5
6
7
8
9
10
/**
 成功和失败的返回
 
 @param isClose 是否关闭SDK的webview(默认为NO)
 @param authSuccess 成功返回
 @param authFailed 失败返回
 */
- (void)addCompletionWebView:(BOOL) isClose sucess:(JDCAuthSuccessBlock) authSuccess failed:(JDCAuthFailedBlock) authFailed;


六、京东授权

1、京东授权逻辑

如果京东已经安装则调起京东App获取a2字符串,获取成功后可以打开URL。

如果未安装则走降级策略,拼出新的URL在App浏览器中直接打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)jdUnionLogin
{
    if (self.isJDAppInstall) {
        NSString *a2String = self.a2String;
        if (![a2String isEmpty]) {
            [self loginWithA2String];
            return;
        }
        [self JDStartAuthorizeScheme:_appScheme];
    } else {
        // 未安装的话 走降级策略
        JDCLogInfo(@"京东app未安装,走降级策略");
        NSString * jdH5LoginUrl = [self getJDH5LoginUrl:self.appID andURL:self.h5DirectUrl];
        if (self.authSuccessBlock) {
            self.authSuccessBlock(nil, jdH5LoginUrl);
        }
        if (!self.isCloseWebView) {
            [JDCURLJumpManager jumpWithUrl:jdH5LoginUrl];
        }
    }
}

2、京东返回处理

为了拆分JD管理类,并保证类的功能的单一性。通过分类来处理京东授权SDK的返回。

(1)在分类JDCAuthManager+authorizeProtocol中处理登录验证回调。

1
2
3
4
5
/**
 * @brief  登录验证回调协议。
 */
@protocol WJLoginVerificationProtocol <NSObject>

(2)在分类JDCAuthManager+jumpTokenProtocol.h中处理H5跳转回调。

1
2
3
4
/**
 * @brief  APP 跳转 H5 回调协议。
 */
@protocol WJLoginJumpTokenProtocol <NSObject>
1
2
3
4
5
/*
* @brief h5跳回app。
 *
 */
@protocol WJLoginH5BackToAppProtocol <NSObject>

(3)在分类JDCAuthManager+verificationProtocol.h中处理token验证和签名接口回调。

1
2
3
4
/**************************
 jd 第三方授权登录业务、验证第三方token接口
 **************************/
@protocol WJLoginAuthorizeProtocol <NSObject>
1
2
3
4
/**************************
 jd 第三方授权登录业务、 验证第三方签名接口
 **************************/
@protocol WJLoginAuthorizeValidateSigntureProtocol <NSObject>

3、AppDelegate处理

为了让用户尽量少写代码的原则。

在分类中,通过运行时的HOOK机制,HOOK了UIApplicationDelegate协议。来处理京东授权成功,成功返回的工作。

问题:用户还是需要实现application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options方法才生效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@implementation JDCAuthManager (AppDelegate)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        KMSwizzleMethod([UIApplication class],
                        @selector(setDelegate:),
                        [JDCAuthManager class],
                        @selector(jdc_hook_setDelegate:));
    });
}

- (void)jdc_hook_setDelegate:(id<UIApplicationDelegate>)delegate{
    [self jdc_hook_setDelegate:delegate];
    
    KMSwizzleMethod([delegate class],
                    @selector(application:openURL:options:),
                    [JDCAuthManager class],
                    @selector(jdc_hook_application:openURL:options:));
}

- (BOOL)jdc_hook_application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
    NSString *openURLString = [url absoluteString];
    NSString *appSchemeFilter = [NSString stringWithFormat:@"%@://",[JDCAuthManager sharedManager].appScheme];
    if ([openURLString hasPrefix:appSchemeFilter]&&[openURLString containsString:@"typeJDAuthLogin?token"]) {
        [[JDCAuthManager sharedManager] handleOpenURL:url];
        JDCLogInfo(@"京东授权成功,成功返回");
        return YES;
    }
    return [self jdc_hook_application:app openURL:url options:options];
}

#pragma mark - Tool

void KMSwizzleMethod(Class originalCls, SEL originalSelector, Class swizzledCls, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(originalCls, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(swizzledCls, swizzledSelector);

    BOOL didAddMethod =
    class_addMethod(originalCls,
                    swizzledSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        Method newMethod = class_getInstanceMethod(originalCls, swizzledSelector);
        method_exchangeImplementations(originalMethod, newMethod);
    }else{
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end