[Flutter-4]HelloVicent——登录认证及本地化存储

124 阅读5分钟

今天是2023-12-25,周一,项目的第四天

预备工作

参考现在运行的apk,跟open api对照,发现开放的Api不够实现完整的一个开源中国,很多需要自己去抓,还有参数也是需要试验来请求得到数据,比较耗时,第3篇做了一个Banner做的时候,花了很多功夫,总结起来就是篇幅很小的一篇短文,但自己会花很多力气;

因此,决定先把Open API提供的接口对应页面的功能实现了先,其他的不管;

所以,现在的方式是,参考Open API,然后找到功能对应的页面,去实现相应的功能,不打算做一个完整的开源中国App了;

当前自我能力评估

我目前的水平是,Dart的语法看了些,widget是完全不懂,内心还是iOS那个UIView的体系,很多功能看页面,完全不知道用什么widget组合起来去实现,希望完成下面的功能,对Flutter的组件能有一定的认知;

接口/功能统计

具体功能如下,共计开放接口42个;

  • 认证接口 2个
  • 个人信息 7个
  • 新闻 2个
  • 帖子 3个
  • 动弹 4个
  • 博客 5个
  • 评论 8个
  • 收藏 3个
  • 软件 4个
  • 私信 2个
  • 搜索 1个
  • 通知 2个

认证登录功能

参看Open API的说明,是通过访问网站用自己的OSChina账号登录,然后进行认证授权的,具体请查阅Open API

同时在应用管理里创建一个应用,回调地址随便填一个能访问的网站就行,提交处于审核中就行,可以有2k次调用,拿到你的应用ID,私钥;

流程

  1. 实现一个WebView,来访问 https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id= {应用ID}&redirect_uri={回调地址}
  2. 网页会跳转到 redirect_uri?code=pVp6Xq&state=;
  3. 截取code字段,拼接到 oauth2_token 的访问网站,https://www.oschina.net/action/openapi/token ?client_id={应用ID} &client_secret={应用私钥}&redirect_uri={回调地址}&code={获取的code}&dataType=json&grant_type=authorization_code
  4. 访问上面的网站正确的话,会返回以下数据,就可以拿到access_token了;
{
    "access_token":"96e09aa9-b3e0-43ae-a884-4a5ffaf74b5c",
    "refresh_token":"db83d6b0-039d-4aaf-995d-2867d2a316b8",
    "uid":2822841,
    "token_type":"bearer",
    "expires_in":604799
}
  1. 对于之后的调用接口出现错误,授权无效之类的,就是token过期,需跳转到登录页面,重新进行以上步骤,获取有效的token授权;

实现代码

  1. 看了 在 Flutter 中使用 webview_flutter 4.0 | js 交互 ,主要是讲webview与flutter交互的问题,截取字段应该有用,先集成三方
webview_flutter: ^4.4.2
  1. 创建一个page,用于展示webview,并编写相关认证的代码,实现同流程所述一步步实现;
  2. WebViewController的代理方法onNavigationRequest实现,对url的拦截;
onNavigationRequest: (NavigationRequest request) {

  String destination = request.url;
  
  //step1 判断是否为回调地址的跳转,并获取code
  if (destination.startsWith('https://www.baidu.com/?code=')) {
  
    print("enter code= $destination");
    int start = 'https://www.baidu.com/?code='.length;
    //step2 截取code
    String code = destination.substring(start, start + 6);
    print("code:$code");

    if (code.isNotEmpty) {
      //step3 访问获取token
      controller.loadRequest(Uri.parse(
          "https://www.oschina.net/action/openapi/token?client_id=$client_id&client_secret=$client_secret&redirect_uri=$redirect_url&code=$code&dataType=json&grant_type=authorization_code"));

      return NavigationDecision.prevent;
    }
  }
  return NavigationDecision.navigate;
},
  1. 然后在 WebViewController的代理方法onPageFinished实现拦截 /action/openapi/token返回的access_token信息,通过集成 shared_preferences 实现本地化存储,封装一个工具类;
shared_preferences: ^2.2.2

工具类实现 LocalStorage.dart

import 'package:shared_preferences/shared_preferences.dart';
class LocalStorage {
  static SharedPreferences? prefs;

  static initSP() async {
    prefs = await SharedPreferences.getInstance();
  }

  static save(String key, String value) {
    prefs?.setString(key, value);
  }

  static get(String key) {
    return prefs?.get(key);
  }

  static remove(String key) {
    prefs?.remove(key);
  }
}

以上参考 浮华_du Flutter中的几种本地持久化存储

代理方法实现:

onPageFinished: (String url) async {

  //step4 判断是否进入action/openapi/token
  if(url.startsWith('https://www.oschina.net/action/openapi/token?')) {
  
    //step5 调用js方法获取到access_token
    String message = await controller.runJavaScriptReturningResult(
        "document.getElementsByTagName('pre')[0].innerText").toString();
    print(message);
    
    //{"access_token":"96e09aa9-b3e0-43ae-a884-4a5ffaf74b5c","refresh_token":"db83d6b0-039d-4aaf-995d-2867d2a316b8","uid":2822841,"token_type":"bearer","expires_in":336581}
    //转为model
    LoginTokenEntity entity = LoginTokenEntity.fromJson(jsonDecode(message));

    //step6 本地化存储access_token,然后返回上个页面
    LocalStorage.save('access_token', entity.accessToken!);
    Navigator.pop(context);
  }
},

整体代码

import 'dart:convert';

import "package:flutter/material.dart";
import 'package:hello_oschina/common/LocalStorage.dart';
import 'package:webview_flutter/webview_flutter.dart';


import '../models/login_token_entity.dart';


class LoginVisitPage extends StatefulWidget {
  const LoginVisitPage({super.key});

  @override
  State<LoginVisitPage> createState() => _LoginVisitPageState();
}

class _LoginVisitPageState extends State<LoginVisitPage> {
  late WebViewController controller;

  String client_id = 'i4rLJxsmURDhf0jRgrYp';
  String client_secret = 'w6Rbh1jDK4dIqlKToAtCMLPHJrI7fsNg';

  String redirect_url = 'https://www.baidu.com/';
  @override
  void initState() {
    // TODO: implement initState
    super.initState();

    controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            // Update loading bar.
          },
          onPageStarted: (String url) {

          },
          onUrlChange: (UrlChange urlChange) {
            // if (urlChange.url.startsWith("https://www.baidu.com/?code=")){
            //
            // }
            print(urlChange.url);
          },
          onPageFinished: (String url) async {
            //step4 判断是否进入action/openapi/token
            if(url.startsWith('https://www.oschina.net/action/openapi/token?')) {
              //step5 调用js方法获取到access_token
              String message = await controller.runJavaScriptReturningResult(
                  "document.getElementsByTagName('pre')[0].innerText").toString();
              print(message);
              //{"access_token":"96e09aa9-b3e0-43ae-a884-4a5ffaf74b5c","refresh_token":"db83d6b0-039d-4aaf-995d-2867d2a316b8","uid":2822841,"token_type":"bearer","expires_in":336581}
              //转为model
              LoginTokenEntity entity = LoginTokenEntity.fromJson(jsonDecode(message));

              //step6 本地化存储access_token
              LocalStorage.save('access_token', entity.accessToken!);
              Navigator.pop(context);
            }
          },
          onWebResourceError: (WebResourceError error) {

          },
          onNavigationRequest: (NavigationRequest request) {
            String destination = request.url;
            //step1 判断是否为回调地址的跳转,并获取code
            if (destination.startsWith('https://www.baidu.com/?code=')) {
              print("enter code= $destination");
              int start = 'https://www.baidu.com/?code='.length;
              //step2 截取code
              String code = destination.substring(start, start + 6);
              print("code:$code");

              if (code.isNotEmpty) {
                //step3 访问获取token
                controller.loadRequest(Uri.parse(
                    "https://www.oschina.net/action/openapi/token?client_id=$client_id&client_secret=$client_secret&redirect_uri=$redirect_url&code=$code&dataType=json&grant_type=authorization_code"));

                return NavigationDecision.prevent;
              }
            }
            return NavigationDecision.navigate;
          },
        ),
      )
      ..loadRequest(Uri.parse('https://www.oschina.net/action/oauth2/authorize?client_id=$client_id&response_type=code&redirect_uri=$redirect_url'));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('开源中国认证授权'),
        titleTextStyle: const TextStyle(color: Colors.black,fontSize: 18.0),
        backgroundColor: Colors.white,
        leading: IconButton(
          icon: const ImageIcon(
            AssetImage("assets/images/btn_back_dark_normal.png"),
            color: Colors.black,
          ),
          onPressed: () {
            Navigator.pop(context);
          },
        ),
      ),
      body: WebViewWidget(controller: controller),
    );
  }
}

额外的收获

js的代码是通过网页的F12进入调试,在调试工具的控制台一个个试得到了完整的返回数据结果;

Screenshot 2023-12-25 at 16.50.09.png

以上