今天是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,私钥;
流程
- 实现一个WebView,来访问
https://www.oschina.net/action/oauth2/authorize?response_type=code&client_id= {应用ID}&redirect_uri={回调地址}; - 网页会跳转到
redirect_uri?code=pVp6Xq&state=; - 截取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 - 访问上面的网站正确的话,会返回以下数据,就可以拿到access_token了;
{
"access_token":"96e09aa9-b3e0-43ae-a884-4a5ffaf74b5c",
"refresh_token":"db83d6b0-039d-4aaf-995d-2867d2a316b8",
"uid":2822841,
"token_type":"bearer",
"expires_in":604799
}
- 对于之后的调用接口出现错误,授权无效之类的,就是token过期,需跳转到登录页面,重新进行以上步骤,获取有效的token授权;
实现代码
- 看了 在 Flutter 中使用 webview_flutter 4.0 | js 交互 ,主要是讲webview与flutter交互的问题,截取字段应该有用,先集成三方;
webview_flutter: ^4.4.2
- 创建一个page,用于展示webview,并编写相关认证的代码,实现同流程所述一步步实现;
- 在
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;
},
- 然后在
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进入调试,在调试工具的控制台一个个试得到了完整的返回数据结果;
以上