Dart-Aqueduct框架开发(八)

声明:本文首发于微信订阅号:Dart客栈,微信后台回复05168获取本篇源码
文章为原创,如需转载请注明出处,并告知作者,谢谢!

1. 介绍

这一节我们来学习一下基于OAuth2.0的用户授权访问

2.什么是OAuth?

我们只需要明确,当用户使用用户名和密码进行登录时,服务端会返回访问令牌token刷新令牌refreshToken访问令牌过期时间给客户端,客户端把令牌保存下来,下次访问向服务器证明已经登录,只需要使用访问令牌进行访问即可,当令牌过期时,我们需要使用刷新令牌,重新把访问令牌请求下来覆盖之前的访问令牌即可,而客户端不需要每次都使用用户名和密码,这个就是主要概念,当然了,为了明确你的应用程序是否可以访问我们的服务器,我们需要在登录的时候在请求头上面添加我在服务器里面声明的包名和密钥进行base64加密,放到key为authorization的请求头里,服务端就会验证你这个客户端是否能访问,以上就是大致流程,下面,我们来实现一下。

3.添加用户模型

在编写授权之前,我们需要添加一个用户模型,使其继承自ManagedObject<T>和实现ManagedAuthResourceOwner<T>,用于表示资源的拥有者,当访问该拥有者名下的资源时,进行授权访问,_User继承的ResourceOwnerTableDefinition主要是表示资源拥有者的身份特征,代码如下:

class User extends ManagedObject<_User>
    implements _User, ManagedAuthResourceOwner<_User> {
  @Serialize(input: true, output: false)  //只能输入不能输出
  String password; //需要的密码
}

class _User extends ResourceOwnerTableDefinition {

  @Column(nullable: true)
  bool isMan; //是否为男

  @Column(nullable: true)
  String nickName; //用户昵称

  @Column(nullable: true)
  String avatar; //头像

  DateTime createTime; //创建时间

  @Column(nullable: true)
  DateTime updateTime; //更新时间

  @Column(nullable: true)
  DateTime lastTime; //最后登录的时间

}

// channel.dart 文件下导入包名,关键
import 'src/entity/user.dart';

4.添加身份认证和授权服务

我们编写完上述的用户模型后,可以在channel.dart文件中初始化身份认证和授权服务,用于当访问需要身份认证才能访问的路由时,可以直接引用得到,代码如下:

  AuthServer _authServer;//授权管理
  ManagedContext context;//可通过该实例操作数据库

  @override
  Future prepare() async {
//...
    final delegate = ManagedAuthDelegate<User>(context, tokenLimit: 20);//tokenLimit用于限制token的长度
    _authServer = AuthServer(delegate);//获取到的授权服务类
//...
  }

然后我们运行aqueduct db generateaqueduct db upgrade这两步命令,将实体类同步到数据库中,这个时候会出现以下表

  • _authclient 用于存储授权的客户端
  • _authtoken 用于存储生成的token
  • _user用户表

5.设置授权的客户端

在建立请求之前,我们需要设置授权的客户端,用于限制哪些客户端才能够访问我们的服务,设置授权客户端有以下形式

  • ID+密钥形式
  • ID形式
  • ID+密钥+重定向形式(后续文章介绍)
  • ID+密钥+范围形式,实现权限管理(后续文章介绍)

1. ID+密钥形式

aqueduct auth add-client --id [你的ID] --secret [你的密钥]

2. ID形式

aqueduct auth add-client --id [你的ID]

3. ID+密钥+重定向形式(后续文章介绍)

aqueduct auth add-client --id [你的ID] --secret [你的密钥] --redirect-uri [你的地址]

4. ID+密钥+范围形式,实现权限管理(后续文章介绍)

 aqueduct auth add-client --id [你的ID] --secret [你的密钥] --allowed-scopes '客户端1 客户端2'

6.实现注册用户

在实现授权登录之前,我们需要注册一个用户,新建一个RegisterController类,添加如下代码

class RegisterController extends ResourceController {
  RegisterController(this.context, this.authServer);

  final ManagedContext context;
  final AuthServer authServer;

  @Operation.post()
  Future<Response> registerUser(@Bind.body() User user) async {
    //过滤掉空值
    if (user.username == null || user.password == null) {
      return Result.errorMsg('用户名或密码不能为空哦!');
    }

    user
      ..salt = AuthUtility.generateRandomSalt() //生成一个随机的盐
      ..hashedPassword =
          authServer.hashPassword(user.password, user.salt) //使用PBKDF2算法进行加密
      ..createTime = DateTime.now()
      ..updateTime = DateTime.now();

    if ((await (Query<User>(context)
              ..where((s) => s.username).identifiedBy(user.username))
            .fetchOne()) !=
        null) { //判断当前用户名已经存在
      return Result.errorMsg("用户名已存在");
    }
    await Query<User>(context, values: user).insert();//插入到数据库中
    return Result.successMsg("注册成功");
  }
}

然后将控制器挂载到路由中,使用/user/register路径进行访问

  @override
  Controller get entryPoint => Router()
//new
  ..route('/user/register').link(()=>RegisterController(context, _authServer));
//new

到目前为止,我们已经实现了注册用户的功能,让我们来访问一下看看吧



可以看到,我们成功的注册了一个用户,下面,我们来添加该接口的客户端访问限制,添加如下代码:

  @override
  Controller get entryPoint => Router()
    ..route('/user/register')
//new
        .link(() => Authorizer.basic(_authServer))
//new
        .link(() => RegisterController(context, _authServer));

当访问路径为/user/register需要在请求头加上authorization:Basic Base64($id:$secret)才可进行访问,例如:我使用com.rhyme.demo客户端ID进行访问,因为没有设置密钥,所以,进行如下base64加密(可以使用这个网站加密)


然后在请求时,如下图所示

7. 实现登录功能(获取token)

实现登录功能,我们可以直接使用AuthController获取授权令牌,所以,添加如下代码

  @override
  Controller get entryPoint => Router()
  //注册用户
    ..route('/user/register')
        .link(() => Authorizer.basic(_authServer))
        .link(() => RegisterController(context, _authServer))
//new
    ..route('/auth/token').link(() => AuthController(_authServer));
//new

AuthController为我们提供三种授权方式:

  • password 使用用户名和密码实现下发授权令牌
  • refresh_token 使用刷新token实现下发授权令牌(后续文章介绍)
  • authorization_code 使用授权码的形式下发授权令牌(后续文章介绍)

所以,我们使用密码的形式请求授权令牌



这里在请求的时候,需要注意以下两点

  • 请求方式为application.x-www-form-urlencoded形式请求
  • 需要携带授权头(即上面注册接口上的请求头)

返回的信息介绍:

  • access_token 可访问的token
  • token_type 令牌类型,默认值为bearer
  • expires_in 过期时间,单位为秒

8.实现授权访问

当访问需要登录(即授权令牌)的路由时,我们可以在路由前添加Authorizer.bearer实现,代码如下:

  //定义路由、请求链接等,在启动期间调用
  @override
  Controller get entryPoint => Router()
//...
//new
    ..route('/articles/[:id]')
        .link(()=>Authorizer.bearer(_authServer))
        .link(() => ArticleController(context));
//new

ArticleController为上几篇文章写的一个文章管理的控制器,熟悉的可以跳过以下内容,该ArticleController内容如下:

class ArticleController extends ResourceController {
  ArticleController(this.context);

  final ManagedContext context;

  @Bind.header("token")
  String token;//@Bind注解可以在局部变量使用,根据传入的key获取对应的值

  @Operation.get() //获取文章列表
  FutureOr<Response> getArticle() async {
//查询文章,并根据createDate进行排序
    final query = Query<Article>(context)
      ..sortBy((e) => e.createDate, QuerySortOrder.ascending);
    final List<Article> articles = await query.fetch();
    return Result.data(articles);
  }

  @Operation.post()//添加一篇文章
  FutureOr<Response> insertArticle(
      @Bind.body(ignore: ["createData"]) Article article) async {
//这里可以直接转为实体,但需要注意的是@Bind.body里的参数含义如下
//ignore表示忽略哪些字段
//reject表示拒绝接收哪些字段
//require表示哪些字段必须有
//啥都不填表示参数如果不传则为空
    article.createDate = DateTime.now();
//插入一条数据
    final result = await context.insertObject<Article>(article);
    return Result.data(result);
  }

  @Operation.get('id')//查询单个文章
  Future<Response> getArticleById(@Bind.path('id') int id) async { //使用中括号表示参数可选
//根据id查询一条数据
    final query = Query<Article>(context)..where((a) => a.id).equalTo(id);
    final article = await query.fetchOne();
    if (article != null) {
      return Result.data(article);
    } else {
      return Result.successMsg();
    }
  }

  @Operation.put()//修改一篇文章
  Future<Response> updateArticleById(
      @Bind.body(ignore: ["createData"]) Article article) async {

    final query = Query<Article>(context)
      ..values.content = article.content
      ..where((a) => a.id).equalTo(article.id);
//更新一条数据
    final result = await query.updateOne();
//    final article = await query.fetchOne();
    if (result != null) {
      return Result.data(result);
    } else {
      return Result.errorMsg("更新失败,数据不存在");
    }
  }

  @Operation.delete('id')//删除一篇文章
  Future<Response> deleteArticleById(@Bind.path('id') int id) async {
    final query = Query<Article>(context)..where((a) => a.id).equalTo(id);
//删除一条数据
    final result = await query.delete();
    if (result != null && result == 1) {
      return Result.successMsg("删除成功");
    } else {
      return Result.errorMsg("删除失败,数据不存在");
    }
  }
}

最后,我们来请求一下看看:



可以看到,成功的返回了内容,以上红色框需要注意:

  • 红框authorization 为表示授权访问
  • 红框OnKXBJ1WyOR2lBrykh1BfcLsdBwDsoqR 为登录成功后返回的access_token,而Bearer为固定写法,Beareraccess_token之间需要加一个空格隔开

以上就是这一节的所有内容,如果小伙伴们觉得有收获,不妨点一下点个赞,让我能看到你跟我一起学习Dart服务器,也是对我写作的一种肯定!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,367评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,959评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,750评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,226评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,252评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,975评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,592评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,497评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,027评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,147评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,274评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,953评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,623评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,143评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,260评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,607评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,271评论 2 358