这一节我们来学习一下基于OAuth2.0
的用户授权访问
我们只需要明确,当用户使用用户名和密码进行登录时,服务端会返回访问令牌token
、刷新令牌refreshToken
、访问令牌过期时间给客户端,客户端把令牌保存下来,下次访问向服务器证明已经登录,只需要使用访问令牌进行访问即可,当令牌过期时,我们需要使用刷新令牌,重新把访问令牌请求下来覆盖之前的访问令牌即可,而客户端不需要每次都使用用户名和密码,这个就是主要概念,当然了,为了明确你的应用程序是否可以访问我们的服务器,我们需要在登录的时候在请求头上面添加我在服务器里面声明的包名和密钥进行base64加密,放到key为authorization
的请求头里,服务端就会验证你这个客户端是否能访问,以上就是大致流程,下面,我们来实现一下。
在编写授权之前,我们需要添加一个用户模型,使其继承自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';
我们编写完上述的用户模型后,可以在channel.dart
文件中初始化身份认证和授权服务,用于当访问需要身份认证才能访问的路由时,可以直接引用得到,代码如下:
AuthServer _authServer;//授权管理
ManagedContext context;//可通过该实例操作数据库
@override
Future prepare() async {
//...
final delegate = ManagedAuthDelegate<User>(context, tokenLimit: 20);//tokenLimit用于限制token的长度
_authServer = AuthServer(delegate);//获取到的授权服务类
//...
}
然后我们运行aqueduct db generate
和aqueduct db upgrade
这两步命令,将实体类同步到数据库中,这个时候会出现以下表
_authclient
用于存储授权的客户端_authtoken
用于存储生成的token_user
用户表在建立请求之前,我们需要设置授权的客户端,用于限制哪些客户端才能够访问我们的服务,设置授权客户端有以下形式
ID+密钥
形式ID
形式ID+密钥+重定向
形式(后续文章介绍)ID+密钥+范围
形式,实现权限管理(后续文章介绍)ID+密钥
形式aqueduct auth add-client --id [你的ID] --secret [你的密钥]
ID
形式aqueduct auth add-client --id [你的ID]
ID+密钥+重定向
形式(后续文章介绍)aqueduct auth add-client --id [你的ID] --secret [你的密钥] --redirect-uri [你的地址]
ID+密钥+范围
形式,实现权限管理(后续文章介绍) aqueduct auth add-client --id [你的ID] --secret [你的密钥] --allowed-scopes '客户端1 客户端2'
在实现授权登录之前,我们需要注册一个用户,新建一个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
加密(可以使用这个网站加密)
然后在请求时,如下图所示
实现登录功能,我们可以直接使用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
可访问的tokentoken_type
令牌类型,默认值为bearer
expires_in
过期时间,单位为秒当访问需要登录(即授权令牌)的路由时,我们可以在路由前添加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
为固定写法,Bearer
和access_token
之间需要加一个空格隔开