使用Flutter进行公司OAuth认证

目录:

[TOC]

公司使用的OAuth认证分为三步


String firstPath = 'http://www.senergychina:15280/oauth-server/oauth!login.action'; 
String secondPath = 'http://www.senergychina:15280/oauth-server/oauth!authorize2.action'; 
String thirdPath = 'http://www.senergychina:15280/stjnhr-web/oauth2-login'; 
String fourthPath = 'http://www.senergychina.com.cn:15280/stjnhr-web/emp!queryAndroidEmpInfo.action?empno=Z00009'; 

要请求人事的接口,就要先通过OAuth认证。 认证流程如下:

st=>start: 开始 
e=>end: 结束 
op1=>operation: 第一步: 请求firstPath,拿到 Response.headers["set-cookie"] op2=>operation: 第二步: 使用Cookie,请求 secondPath,返回code op3=>operation: 第三步: 使用code,请求thirdPath。 thirdPath会重定向到另一个地址。 这里我们要拿到thirdPath的 headers["set-cookie"] 
op4=>operation: 第四步: 将第三步获取的cookie,放到headers["cookies"]中. 请求需要的接口。 
st->op1->op2->op3->op4->e 

操作代码如下:

第一步

 第一步,拿cookie: 
Map<String, String> firstParams = { 'username': 'W00001', 'password': '111111' }; 
Response res; 
try { res = await dio.get(firstPath, queryParameters: firstParams);
  firstCookies = res.headers[HttpHeaders.setCookieHeader]; 
  List<Cookie> cookies = resultCookies.toList(); 
// 请求第二步 
  oauthSecond(); 
} 
on DioError catch (error) { } }

[图片上传失败...(image-656ac3-1553874001107)] > #
关于Cookie

cookie值存放在response.header中,可以使用res.headers["set-cookies"]拿到返回值中的set-cookie,也可以导入 import 'dart:io'; 来使用res.headers[HttpHeaders.setCookieHeader]获取。
获取到的cookie返回值是List<String>,如果要存放到本地,需要使用CookieJar(); 具体使用方法<https://github.com/flutterchina/cookie_jar
CookieJar存储的时候,要传入List<Cookie>,而从header中获取的类型是List<String>。所以中间要进行转换。代码如下

CookieJar 的使用
 import 'package:cookie_jar/cookie_jar.dart'; 
void main() async { 
  List<Cookie> cookies = [new Cookie("name", "wendux"),new Cookie("location", "china")]; 
  var cj = new CookieJar();
  //Save cookies cj.saveFromResponse(Uri.parse("https://www.baidu.com/"), cookies); 
  //Get cookies 
  List<Cookie> results = cj.loadForRequest(Uri.parse("https://www.baidu.com/xx")); 
  print(results); 
} 
# List<String> 转换为 List<Cookie> 
_updateCookie(Response response) { 
  resultCookies = {}; 
  List<String> allCookie = response.headers[HttpHeaders.setCookieHeader]; 
  if (allCookie != null && allCookie.isNotEmpty) { 
    for (String setCookie in allCookie) {
      print('每一个cookie:' + setCookie.toString());
      var cookies = setCookie.split(";"); 
      for (String rowCookie in cookies) {
         _setCookie(rowCookie); 
      } 
    } 
  } 
} 

_setCookie(String rowCookie) { 
  if (rowCookie.length > 0) { 
    var keyValue = rowCookie.split("=");
     if (keyValue.length == 2) { 
      var key = keyValue[0].trim(); 
      var value = keyValue[1]; 
      if (key == 'Path' || key == 'Expires') { 
        return; 
      } 
     cookie = Cookie(key, value); 
     resultCookies.add(cookie); 
    } 
  } 
} 

第二步

使用第一步获取的Cookie请求code

oauthSecond() async { 
  Response res; 
  Map<String, String> params = { 
    'clientId': "c1ebe466-1cdc-4bd3-ab69-77c3561b9dee", 
    'responseType': "code" 
  }; 
  Options options = Options(); 
  options.headers[HttpHeaders.cookieHeader] = firstCookies; 
  try { 
    res = await dio.get(secondPath, queryParameters: params, options: options); 
    code = res.toString(); 
    print('第二步成功:' + res.toString()); 
    oauthThird(); 
  } 
  catch (error) { 
    print('第二步失败:' + error.toString()); 
  }; 
} 

获取response的Cookie,我们使用HttpHeaders.setCookieHeader,而将cookie添加到header的时候,我们使用HttpHeaders.cookieHeader。
也就是下图的位置,当然使用setCookieHeader也可以。
[图片上传失败...(image-323c6d-1553874001107)]
Dio的Header放在options中。如果接口需要传入固定的cookie,那就可以在初始化的时候,直接写入。或者像步骤二,在请求的时候带入。

第三步 根据第二步返回的code,请求要一直使用的cookie

oauthThird() async {     
  Response res; 
  Map<String, String> params = { 'code': code, }; 
  try{ 
    BaseOptions op = dio.options; 
    op.followRedirects = false; 
    res = await dio.get(thirdPath, queryParameters: params); 
    print('第三步headers的值是:' + res.headers.toString()); 
    print('第三步realUri: ' + res.realUri.toString()); 
    var redirects = res.redirects; 
    for (var redirect in redirects) { 
    print('第三步重定向信息:{method:${redirect.method}, Code: ${redirect.statusCode}, ${redirect.location}'); 
  }   
  _updateCookie(res); 
  print("第三步成功之后的cookies的值是:${resultCookies.toString()}"); 
  print('是否重定向:' + res.isRedirect.toString()); 
  /// 如果有Cookie就可以正常请求数据 oauthFourth(); 
} 
on DioError catch (error) { 
  if (error.response.statusCode == 302) { 
    persistentCookies = error.response.headers[HttpHeaders.setCookieHeader];
    oauthFourth();      
  } 
} 

这一步,默认的会重定向到网页。即302.

难以解决的问题就是:我们需要的是第三步接口返回的Cookie,但是重定向完成之后,response 是重定向后的Response,而不是第三步接口的response。

获取重定向后302,前一个页面的Cookie。

解决:在options中,有一个参数followRedirects,这个参数默认是true,也就是允许重定向,还有一个maxRedirects参数,是允许重定向的次数。我们可以将followRedirects设置为false,不允许重定向,那么接口就会在请求完成之后停止。所以可以在catch中获取到DioErrorType.Response,在catch中判断,如果是重定向,就从error中拿cookie。至此,我们就拿到了cookie。

优化

优化1:

最后一步的时候,302会进入catch中,作为错误返回给我们,但是这并不是我们想要的。 Dio的options中,有一个validateStatus,这个callBack允许用户直接设置返回值中的StatusCode是多少的时候进success,其他的进failure。使用如下

baseOptions.validateStatus = (int state) { 
  return (state == 302 || state == 200); 
}; 

所以我们可以将302作为成功返回,这样我们就不必在catch中获取,而是像第一步一样,直接使用res.headers[HttpHeaders.setCookieHeader]即可。

优化2

由于请求是异步的,所以每次请求可以使用Future来优化写法。

try { 
  res = await dio.get(firstPath, queryParameters: firstParams); 
  firstCookies = res.headers[HttpHeaders.setCookieHeader]; 
  List<Cookie> cookies = resultCookies.toList(); 
  // 请求第二步 oauthSecond(); 
} 
on DioError catch (error) { } 

可优化为:

tokenDio.get(firstPath,queryParameters: firstParams).then((res){ 
  print('第一步成功:' + res.headers[HttpHeaders.setCookieHeader].toString());
  baseOptions.headers[HttpHeaders.cookieHeader] = res.headers[HttpHeaders.setCookieHeader]; 
  return tokenDio.get(secondPath, queryParameters: secondParams); 
})
.catchError((Object error){ 
  print('捕捉到错误' + error.toString()); 
}).whenComplete(() {}) 

同时,这种写法可以串联多个,正好符合认证的流程,代码如下

tokenDio.options = baseOptions; baseOptions.validateStatus = (int state) { 
  return (state == 302 || state == 200); 
}; 
tokenDio.get(firstPath,queryParameters: firstParams).then((res){ 
  print('第一步成功:' + res.headers[HttpHeaders.setCookieHeader].toString());
  baseOptions.headers[HttpHeaders.cookieHeader] = res.headers[HttpHeaders.setCookieHeader]; 
  return tokenDio.get(secondPath, queryParameters: secondParams); 
}).then((res){ 
  print('第二步code的值是:' + res.toString()); 
  baseOptions.followRedirects = false; 
  Map<String, String> thirdParams = { 'code': res.toString(), }; 
  return tokenDio.get(thirdPath, queryParameters: thirdParams); 
}).then((Response res){ 
  // 将BaseOptions重置 baseOptions = BaseOptions(); 
  print('这时候的res是:' + res.headers[HttpHeaders.setCookieHeader].toString());
  tokenDio.options.headers[HttpHeaders.cookieHeader] = persistentCookies = options.headers[HttpHeaders.cookieHeader] = persistentCookies = res.headers[HttpHeaders.setCookieHeader]; 
  fetchFourth(); 
})
.catchError((Object error){ 
  print('捕捉到错误' + error.toString()); 
}).whenComplete(() {}); 

串联的时候,每次返回新的请求,就可以继续串联then,进行操作。而且经过优化1的操作,我们可以在第三步直接在then里操作,如果没有优化1的操作,我们第三步重定向的结果,就需要在catch中处理,也就无法继续串联下去。

优化3

Dio2.0版本以后,可以使用intercepters,我们可以在intercepter中拦截处理:

dio.interceptors 
  ..add(CookieManager(CookieJar())) 
  ..add(InterceptorsWrapper( 
    onRequest: (RequestOptions options) { 
      print("请求前:" + options.toString()); 
      return options; 
    }, 
    onResponse: (Response response){ 
      print("请求结果:" + response.headers[HttpHeaders.setCookieHeader].toString()); 
    }, 
    onError: (error){ 
      print("请求错误:" + error.toString()); 
    }), 
  ); 

那么,我们就可以进一步进行优化。 如果第一次本地没有请求的缓存,我们就可以在请求前进行判断,然后进行认证: 代码如下

Dio _dio; 
BaseOptions baseOptions = BaseOptions(); 
List<String> persistentCookies = []; 
Dio tokenDio = Dio(); 
Dio get dio { 
  if (_dio != null) { 
    return _dio; 
  } 
  // persistentCookies = []; 
  _dio = Dio(); 
  if (persistentCookies.isNotEmpty) { 
    baseOptions.headers[HttpHeaders.cookieHeader] = persistentCookies;     
  } 
  _dio.interceptors 
  ..add(InterceptorsWrapper(
    onRequest: (RequestOptions options){ 
      if (persistentCookies.isEmpty) { 
        (tokenDio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { 
        client.findProxy = (uri) { 
          return "PROXY 192.168.40.83:8888"; 
        }; 
      }; 

    tokenDio.options = baseOptions; 
    baseOptions.validateStatus = (int state) { 
      return (state == 302 || state == 200); 
    }; 

    dio.lock(); 
    return tokenDio.get(firstPath,queryParameters: firstParams).then((res){ 
        print('第一步成功:' + res.headers[HttpHeaders.setCookieHeader].toString());
        baseOptions.headers[HttpHeaders.cookieHeader] = res.headers[HttpHeaders.setCookieHeader]; 
      return tokenDio.get(secondPath, queryParameters: secondParams); }).then((res){ 
        print('第二步code的值是:' + res.toString()); 
        baseOptions.followRedirects = false; 
        Map<String, String> thirdParams = { 'code': res.toString(), }; 
        return tokenDio.get(thirdPath, queryParameters: thirdParams); 
      }).then((Response res){ 
      // 将BaseOptions重置 baseOptions = BaseOptions(); 
      print('这时候的res是:' + res.headers[HttpHeaders.setCookieHeader].toString());
       tokenDio.options.headers[HttpHeaders.cookieHeader] = persistentCookies = options.headers[HttpHeaders.cookieHeader] = persistentCookies = res.headers[HttpHeaders.setCookieHeader]; 
        return options; 
      }).catchError((Object error){ 
        print('捕捉到错误' + error.toString()); 
      }).whenComplete(() => dio.unlock()); 
    } 
    else { 
      options.headers[HttpHeaders.cookieHeader] = persistentCookies; 
      return options; 
    } },
  onResponse: (Response response){ }, 
  onError: (Object error){ })); 

  return _dio;
} 

getData() async { 
  Response res; 
  try { 
    res = await dio.get(fourthPath); 
    print('第四步的结果:' + res.toString()); 
  } 
  catch(error){ 
    print('第四步错误:' + error.toString()); 
  }; 
} 

使用的时候,直接调用 getData()请求数据即可。 第一次的时候,由于persistentCookie本地没有数据,所以会自动认证。

注意:

  • 这里使用了两个dio对象请求,tokenDio专门用来请求token。
  • onRequest: 的callBack里面,必须返回options,即onRequest: (RequestOptions options),改造这里面的options。如果返回baseOptions,请求会无法继续
  • dio.lock();可以暂时上锁,在认证完成后解锁,即可继续进行请求。 这里只是暂时对请求前没有cookie进行了封装。如果是cookie过期,可以根据返回的值再次进行认证,即在onResponse: (Response response){}这个callBack里操作。

文档:

  1. 将String转化为Cookie: https://stackoverflow.com/questions/52241089/how-do-i-make-an-http-request-using-cookies-on-flutter
  2. CookieJar: https://github.com/flutterchina/cookie_jar
  3. Dio: https://github.com/flutterchina/dio#cookie-manager * Dio里面的issue回答,也很有用!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,478评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,825评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,482评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,726评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,633评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,018评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,168评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,320评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,264评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,288评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,995评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,587评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,909评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,284评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,862评论 2 339