Flutter使用Dio拦截器实现token验证过期的功能

原创
2019/04/19 12:02
阅读数 1.7W

前言:

     之前分享过在Android中使用Retrofit实现token失效刷新的处理方案,现在Flutter项目也有“token验证过期”的需求。刚开始我使用的是EventBus来通知弹出登录页面,但是发现在refresh token过期后并没有去登录,原因是EventBus需要在有生命周期的State状态中才能发送消息,在这里我构造了全局的上下文,以便弹出登录页面。所以接下来我简单总结一下在Flutter项目中如何实现自动刷新token并重发请求的拦截器功能,希望对大家有所帮助。

需求:

     1.有两个token, 分别为access_token和refresh_Token, access_token的有效期为1小时,refresh_Token的有效期为7天;

     2.如果access_token1个小时后过期了,服务器会返回401,此时客户端要根据刷新access_token的retrofit接口去重新请求新的access_token;

     3.如果refresh_Token7天后也过期了,则要求跳到登录页面。

思路:     

     1.Dio实现网络请求

     2.自定义token拦截器,实现token自动刷新并重发请求

     3.refreshToken过期,弹出登录页面。

实现的步骤:

1.配置Android目录的gradle依赖

dependencies {
    ...
    implementation 'de.greenrobot:eventbus:3.0.0-beta1'
}

2.在pubspec.yaml添加sdk

dependencies:
  ...
  cupertino_icons: ^0.1.0
  dio: ^2.1.7

3.封装一个获取新的accessToken方法

 Future<String> getToken() async {
    String refreshToken = DataUtil.getRefreshToken; //获取当前的refreshToken
    String accessToken;

    Dio tokenDio = new Dio(); //创建新Dio实例
    tokenDio.options.headers['refresh-token'] = refreshToken;//设置当前的refreshToken

    try {
      String url = url; //refreshToken url
      var response = await tokenDio.get(url); //请求refreshToken刷新的接口
      accessToken = response.data['access_token']; //新的accessToken
      refreshToken = response.data['refresh_token'];//新的refreshToken
      DataUtil.saveRefreshToken(refreshToken); //保存新的refreshToken
    } on DioError catch (e) {    
       if(e.response.statusCode==401){ //401代表refresh_token过期
        //refreshToken过期,弹出登录页面
        //解决方法一:封装一个全局的context
        //return Navigator.of(Util.context).push(new MaterialPageRoute( builder: (ctx) => new LoginPage())); 
        //解决方法二:当refresh token过期,把退出的登录的操作放在dio网络请求的工具类去操作
      }
    }
    return accessToken;
  }

 4.token失效时,异步获取accessToken

onError(DioError error) async { 
    if (error.response != null && error.response.statusCode == 401) { 401代表access token过期
      Dio dio = DioUtil().dio;//获取Dio单例
      dio.lock(); //加锁
      String accessToken = await getToken(); //异步获取新的accessToken
      ...
      dio.unlock(); //解锁
    }
    super.onError(error);
  }

5.重新发起一个请求获取数据

      //重新发起一个请求获取数据
      Dio tokenDio2 = new Dio(); //创建新的dio实例
      tokenDio2.options.headers['access-token'] = accessToken;
      try {
        var newRequest = await tokenDio2.request(request.path);
        return newRequest;
      } on DioError catch (e) {
        return e;
     }

6.Dio拦截器的完整代码

typedef void ChildContext(BuildContext context);

class TokenInterceptor extends Interceptor {
  ChildContext context; //上下文

  @override
  onError(DioError error) async {
    if (error.response != null && error.response.statusCode == 401) { //401代表token过期
      Dio dio = DioUtil().dio;//获取Dio单例
      dio.lock(); //加锁
      String accessToken = await getToken(); //异步获取新的accessToken

      //重新发起一个请求获取数据
      Dio tokenDio2 = new Dio();//创建新的Dio实例
      tokenDio2.options.headers['access-token'] = accessToken;

      try {
        var newRequest = await tokenDio2.request(request.path);
        return newRequest;
      } on DioError catch (e) {
        return e;
      }
      dio.unlock(); //解锁
    }
    super.onError(error);
  }


  Future<String> getToken() async { 
    String refreshToken = DataUtil.getRefreshToken; //获取当前的refreshToken
     String accessToken ;

    Dio tokenDio =new Dio(); //创建新的Dio实例
    tokenDio.options.headers['refresh-token'] = refreshToken ;//设置当前的refreshToken 

    try {
      String url = url; //refreshToken url
      var response = await tokenDio.get(url); //请求refreshToken刷新的接口
      accessToken = response.data['access_token']; //新的accessToken
      refreshToken = response.data['refresh_token'];//新的refreshToken
      DataUtil.saveAccessToken(accessToken); //保存新的accessToken
      DataUtil.saveRefreshToken(refreshToken); //保存新的refreshToken
    } on DioError catch (e) {
      if(e.response.statusCode==401){//401代表refresh_token过期
        //refreshToken过期,弹出登录页面
        //解决方法一:封装一个全局的context
        //return Navigator.of(Util.context).push(new MaterialPageRoute( builder: (ctx) => new LoginPage())); 
        //解决方法二:当refresh token过期,把退出的登录的操作放在dio网络请求的工具类去操作
      }
    }
    return accessToken;
  }

}

7.在网络请求的工具类添加自定义的token拦截器

/*
 *Dio网络请求的工具类
 */
class DioUtil {
  Dio dio;
  static DioUtil _instance;

  static DioUtil getInstance() {
    if (_instance == null) {
      _instance = DioUtil();
    }
    return _instance;
  }

  //get方法
  Future<Response> get(url, {data, options, cancelToken}) async {
   String accessToken = DataUtil.getAccessToken; //获取当前的accessToken 

    options = BaseOptions(
      connectTimeout: 15000,
      headers: {
        Constants.accessToken: accessToken
      },
    );

    dio = new Dio(options);

    //添加自定义的token拦截器
    dio.interceptors.add(new TokenInterceptor());

    Response response;
    try {
      response = await dio.get(url, cancelToken: cancelToken);
    } on DioError catch (e) {
      print(e.response.data);
    }
    return response;
  }

}

8.总结

在Flutter项目中自定义一个自动刷新并重发请求的Dio拦截器,经过不断调试,最终实现了功能。如果有什么疑问的话,欢迎留言联系我哦!

展开阅读全文
打赏
2
5 收藏
分享
加载中
该评论暂时无法显示,详情咨询 QQ 群:912889742
EmilyWu博主
1.判断,当dialog不为空的情况不再去实例化; 2.给dialog添加时间机制。 (你先试试,看能不能解决)
2020/11/18 10:26
回复
举报
额,可能我没说清楚,token失效后跳转登录页面,不是弹dialog。我找了下资料用Navigator.pushNamedAndRemoveUntil,但是这个方法清空了栈,要是不点登录按返回键就直接回到桌面了......
2020/11/18 15:25
回复
举报
EmilyWu博主
我在处理退出登录的方式如下(你可以参考一下) 1.Android文件夹的清单文件设置launchMode为singleTop模式 2.在Application封装navigateToClearStack方法(在MaterialApp设置Application) static FluroRouter router; static navigateToClearStack( @required BuildContext context, @required String route, clearStack = true, transition = TransitionType.inFromRight}) { router.navigateTo(context, route, clearStack: clearStack, transition: transition); } 3.封装Routes class Routes { static String login = "/LoginPage"; static void configureRoutes(FluroRouter router) { router.define(login, handler: loginRouteHandler); router.notFoundHandler = Handler( handlerFunc: (BuildContext context, Map> params) { return; }); } } var loginRouteHandler = Handler( handlerFunc: (BuildContext context, Map> params) { return LoginPage(); }); 4.退出登录的调用 navigateToClearStackPage(context, "${Routes.login}");
2020/11/18 16:12
回复
举报
好的,我先试试,感谢!
2020/11/18 16:14
回复
举报
eventbus是android里面的 打包运行到ios上还能生效吗?
2020/06/05 09:52
回复
举报
EmilyWu博主
你要在iOS的文件夹导入IMXEventBus,写法也比较简单
2020/06/05 10:20
回复
举报
只会android 不懂ios😭
2020/06/05 11:22
回复
举报
EmilyWu博主
我刚刚看了代码,我误解你的意思啦,不好意思哦!android和iOS不用处理的,在Flutter的lib里面处理eventbus
2020/06/05 11:57
回复
举报
好的 Thanks♪(・ω・)ノ
2020/06/09 16:05
回复
举报
该评论暂时无法显示,详情咨询 QQ 群:912889742
请问一下,datautil类代码在哪里,小白。。是不是里面就是accessToken以及refreshToken以及save和个方法啊
2019/11/17 19:03
回复
举报
EmilyWu博主
是的,datautil类就是存储数据
2019/11/18 10:45
回复
举报
return Navigator.of(context).push(new MaterialPageRoute(
builder: (ctx) => new LoginPage()));

请问这个 “context”上下文对象,怎么获取到的尼?
2019/10/19 09:10
回复
举报
EmilyWu博主
直接输入context就可以啊
2019/10/19 20:56
回复
举报
typedef void ChildContext(BuildContext context) ChildContext context; //上下文 这个里是您构造的上下文,这个上下文是在哪里进行实例化? ``` dart Navigator.of(context).push(new MaterialPageRoute( builder: (ctx) => new LoginPage())); ``` 这里就报错了,提示 ``context`` 是一个 void ChildContext 不是一个 BuildContext 类型.
2019/10/22 09:18
回复
举报
EmilyWu博主
解决方法一:封装一个全局的context; 解决方法二:当refresh token过期,把退出的登录的操作放在dio网络请求的工具类去操作。
2019/10/22 11:08
回复
举报
该评论暂时无法显示,详情咨询 QQ 群:912889742
EmilyWu博主
解决方法二:封装一个Dioutil,在GET或者POST方法里面传入BuildContext context,在页面调用的时候传入context就行了
2019/10/22 15:28
回复
举报
更多评论
打赏
19 评论
5 收藏
2
分享
返回顶部
顶部