Dart 的服务端开发
本文视频
本文已录制视频上传B站,请配合视频食用 《Dart 全栈之服务端》
本文是博主《Flutter全栈式开发》系列课程的拓展。我们在Flutter课程中,编写了一个注册登录案例,详细讲解了常见的注册、登录、API接口鉴权功能,前后端是如何配合实现的。其中后台服务使用了Dart语言中较为知名的aqueduct框架。
近来由于Dart版本迭代过于频繁和激烈,尤其是不兼容的空安全特性,导致课程中使用的服务端框架aqueduct停止维护。维护该框架的公司没有多余的精力使之兼容新版本,目前亦无社区接手项目,可能永久停止。相继的,Dart的第二大服务端框架angel亦停止维护,不得不说是Dart的一大遗憾。我们的课程已经发布有较长时间,其中的案例,现在只能使用旧版本才能运行。
为了大家有更好的课程体验,能持续跟进Dart的新特性,我特别编写了一个轻量级的支持空安全特性的Dart HTTP服务器框架:arowana,它可以直接在Flutter中使用,可以运行在移动端。
arowana
这个单词是龙鱼的意思。龙鱼最早见于我国山海经记载:
“龙鱼陵居在其北,状如狸。一曰“鱼段”。即有神圣乘此以行九野。”
龙鱼有须,胸鳍似龙爪,加上浑身金鳞(或红鳞),因而形似神龙。加上其盘旋灵动,华贵端庄,静时安若处子,动时迅如脱兔,所以其神更似神龙。目前龙鱼为濒危物种,列入《世界自然保护联盟》(IUCN) 濒危物种红色名录。
arowana 框架基于Dart语言官方提供的 shelf 库,以处理HTTP请求,但shelf功能较弱,arowana 对其进行了部分功能增强。同时,还参考了我较为欣赏的Go语言Gin框架,编写了一个基于前缀树搜索的高性能路由组件,完全避免了正则匹配。我们知道,Dart语言中的正则处理性能较差,在AOT编译时可能还会称为性能瓶颈。arowana中还参考了aqueduct,封装了对Isolate并发处理的操作,使得并发处理更加简单。arowana没有太多依赖,尤其没有依赖一些动态特性(反射),因此它可以在移动端运行。
众所周知,Dart语言非常缺乏一个好的数据库ORM框架,其中一部分原因是因为Dart语言反射能力较差,甚至在Flutter中无法使用反射。因此arowana 并未提供或整合数据库模块,大家可自由组合目前可用的数据库连接模块,使用SQL操作数据。下面看一个简单示例:
class MyAChannel extends DefaultChannel {
@override
Future prepare() async {
print('current isolate [${Isolate.current.debugName}]');
}
@override
void entryPoint() {
// 注册get请求路由
get('/hello', (r) {
return Response.ok('<h1>Hello, arowana!</h1>',
headers: {'content-type': 'text/html; charset=UTF-8'});
});
}
}
在Flutter中使用:
void main() {
var app = Application(MyAChannel());
// numberOfInstances: 启动两个后台isolate处理请求
app.start(numberOfInstances: 2,consoleLogging: true);
runApp(const MyApp());
}
注册登录实现
来看一个完整例子,我们用arowana重写《Flutter全栈式开发》课程中的注册登录案例:
添加依赖。目前arowana处于试验版,没有发布仓库,使用Git协议添加依赖:
dependencies:
dart_jsonwebtoken: ^2.3.2
arowana:
git: https://github.com/arcticfox1919/arowana.git
sqlite3: ^1.2.0
crypto: ^3.0.1
代码结构:
首先编写一个Channel,注册路由:
/// server.dart
class MyAChannel extends DefaultChannel{
late SqliteDb db;
@override
Future prepare() async{
db = SqliteDb.Connect();
}
@override
void entryPoint() {
post('/register', RegisterController(db));
post('/login', LoginController(db));
// 分组路由
var r = group('/info');
// 给该组设置鉴权中间件
r.use(Auth.bearer(AuthVerifier()));
r.get('/list', (request){
return ResponseX.ok({
'language': [
{'id': 1, 'name': 'dart'},
{'id': 2, 'name': 'java'},
{'id': 3, 'name': 'c'},
{'id': 4, 'name': 'golang'},
{'id': 5, 'name': 'python'}
]
});
});
}
}
路由使用,与Gin相似,这里使用了分组路由,给'/info'
下的所有子路由设置了一个身份验证的中间件,访问子路由必须鉴权。arowana 提供了一个简单的身份验证中间件Auth
。开发者只需要实现一个自己的验证器AuthVerifier
,从而自定义验证逻辑,它需要继承自AuthValidator
。
两个post
请求的路由分别实现注册和登录功能,两者逻辑相似,都使用json格式传递数据:
/// ctrl.dart
class RegisterController{
SqliteDb db;
RegisterController(this.db);
Future<Response> call(Request request)async{
var body = await request.body;
var json = body.json;
if (json != null) {
// 从请求参数中构造User实体类
var user = User.from(json);
if (user.check()) {
user.save(db);
// 注册成功,给客户端返回令牌
return ResponseX.token(user.generateToken());
}
}
return ResponseX.badRequest('Invalid username or password!');
}
}
class LoginController{
SqliteDb db;
LoginController(this.db);
Future<Response> call(Request request) async {
var body = await request.body;
var json = body.json;
if (json != null) {
var user = User.from(json);
if (user.verify(db)) {
// 登录成功,给客户端返回令牌
return ResponseX.token(user.generateToken());
}
}
return ResponseX.badRequest('Invalid username or password!');
}
}
如果你使用表单提交用户名和密码,你的请求头中需要指定application/x-www-form-urlencode
类型,在服务端,你需要使用下面的方式获取参数:
var body = await request.body;
var form = body.formParams;
if (form != null) {
print(form['uname']);
print(form['passwd']);
}
看一下User
类的逻辑:
class User {
int? id;
String? uname;
String? passwd;
User.from(Map<String, dynamic> json) {
uname = json['uname'];
passwd = json['passwd'];
}
// 检查是否提交了有效的用户名、密码
bool check() =>
uname != null &&
uname!.isNotEmpty &&
passwd != null &&
passwd!.isNotEmpty;
// 验证登录
bool verify(SqliteDb db) {
if (!check()) return false;
// 对密码进行hash处理
passwd = md5.convert(utf8.encode(passwd!)).toString();
// 去数据库查询用户名是否注册,密码是否正确
var u = db.getUser(this);
if(u == null) return false;
id = u.id;
return true;
}
// 保存用户名、密码到数据库
void save(SqliteDb db){
if(check()){
// hash passwd
passwd = md5.convert(utf8.encode(passwd!)).toString();
var u = db.save(this);
id = u.id;
}
}
// 生成JWT格式令牌
AuthToken generateToken({Duration expiration = const Duration(hours: 24)}) {
var now = DateTime.now();
var expirationDate = now.add(expiration);
var t = createToken(
this, expirationDate.millisecondsSinceEpoch~/1000);
return AuthToken(t, now, expirationDate);
}
}
逻辑比较简单,这里主要使用了JWT格式生成Token,我们Flutter课程中已经介绍过JWT,但课程当时的案例用的另一种OAuth2.0生成的Token。
最后看一下验证器的实现:
class AuthVerifier extends AuthValidator{
@override
FutureOr<Authorization?> validate<T>(AuthorizationParser<T> parser, T authorizationData) {
if (parser is AuthorizationBearerParser) {
return _verify(authorizationData as String);
}
throw ArgumentError(
"Invalid 'parser' for 'AuthValidator.validate'. Use 'AuthorizationBearerHeader'.");
}
FutureOr<Authorization?> _verify(String accessToken) async {
try {
// 校验客户端带来的Token
final jwt = verifyToken(accessToken);
return Authorization((jwt.payload as Map)['id'].toString(), this);
} on JWTUndefinedError catch(e){
print(e.error);
// 校验失败,颁发的Token已经过期了,客户端需要刷新令牌或者重新登录
if(e.error is JWTExpiredError){
throw TokenExpiredException('Error: the token has expired, please refresh');
}
}
}
}
这里,对JWT Token的处理,使用了另一个库 dart_jsonwebtoken。给Auth
中间件提供我们自定义的验证器,当访问注册了身份验证中间件的子路由/info/list
时,首先会校验请求的令牌,未登录或令牌过期,则校验失败,无权访问该路由。
最后,我们可以使用Postman工具来检验成果:
访问需要鉴权的接口
完整示例代码,请访问 这里
关注公众号:编程之路从0到1
课程后续会不断更新,持续跟进Flutter技术迭代,关注我,你不会失望!
或关注博主的视频课程