Dart全栈系列 Aqueduct(四)简单配置OAuth 2.0
曾经有位大佬说过,java之所以能纵横江湖几十年,完全是因为Spring太牛逼了。而Aqueduct就是Dart界的SpringBoot。
目录
什么是OAuth 2.0
请看阮一峰老师的博客
阮一峰老师的解释非常通俗了,这里我更加通俗的解释一下:OAuth是江湖上非常流行的,用来验证用户权限的协议,我们常用这玩意做注册和登陆。为什么要用它?因为它已经是事实上的标准化了,支持自己和第三方登陆、单点登陆,而且安全性很高!
那不用行不行?当然可以,愿意当非主流,谁也拦不住你!
OAuth 2.0的四种验证方式
具体请参考阮老师的博客,这里不再赘述。下面是整理出来供我自己用的:
方式 | 用途 |
---|---|
授权码 | 常用于第三方登陆认证 |
隐藏式 | 纯前段应用 |
密码式 | 高度信任某应用,或自家应用 |
客户端凭证 | 适用于没有缓存的应用,如控制台命令行应用 |
Aqueduct引入Oauth2.0
创建用户实体、数据库表
import 'package:aqueduct/managed_auth.dart';
import 'package:heroes/heroes.dart';
import 'package:heroes/model/hero.dart';
class User extends ManagedObject<_User> implements _User, ManagedAuthResourceOwner<_User> {}
class _User extends ResourceOwnerTableDefinition {}
这里创建了一个model——User,必须继承ManagedAuthResourceOwner
。这是个Aqueduct内置的托管类,里面实现了OAuth2.0的一系列方法。
同时数据库接口_User
也要继承ResourceOwnerTableDefinition
,里面封装着对认证信息的操作。
然后就可以更新数据库了:
aqueduct db generate
aqueduct db upgrade --connect postgres://heroes_user:password@localhost:5432/heroes
这时migrations的版本号就变成了2.
这里我试了好几次也不成功,后来删除了00000001_initial.migration.dart,再重新生成它,然后手动把文件名里的1改成了2,再去同步数据库,没想到竟然成功了。
这里有个问题,我们登陆时会提交用户名和密码,但是User继承自_User继承于ResourceOwnerTableDefinition,ResourceOwnerTableDefinition中没有存用户密码,只存了一个hashedPassword。这是因为真正的用户密码我们是不应该存的,而应该存hash加密后的密码。所以我们应该在User中添加个password用来接收用户提交的密码,但却不直接存入数据库:
class User extends ManagedObject<_User> implements _User, ManagedAuthResourceOwner<_User> {
@Serialize(input: true, output: false)
String password;
}
创建用户注册Controller
import 'dart:async';
import 'package:aqueduct/aqueduct.dart';
import 'package:heroes/model/user.dart';
class RegisterController extends ResourceController {
RegisterController(this.context, this.authServer);
final ManagedContext context;
final AuthServer authServer;
@Operation.post()
Future<Response> createUser(@Bind.body() User user) async {
// Check for required parameters before we spend time hashing
if (user.username == null || user.password == null) {
return Response.badRequest(
body: {"error": "username and password required."});
}
user
..salt = AuthUtility.generateRandomSalt()
..hashedPassword = authServer.hashPassword(user.password, user.salt);
return Response.ok(await Query(context, values: user).insert());
}
}
这个controller主要是通过构造方法注入了一个AuthServer
,然后用这玩意去哈希加密用户密码,再存入数据库。
这里有个salt是盐的意思,它是个32位Base64编码的随机序列。但是为什么叫做「盐」呢,我觉得叫「润色」更为合适。
接下来在channel中挂载router:
import 'package:heroes/controller/register_controller.dart';
...
@override
Controller get entryPoint {
final router = Router();
router
.route('/heroes/[:id]')
.link(() => HeroesController(context));
router
.route('/register')
.link(() => RegisterController(context, authServer));
return router;
}
}
注册用户
curl -X POST http://localhost:8888/register -H 'Content-Type: application/json' -d '{"username":"bob", "password":"password"}'
执行完上面的命令后,就可以在数据库的_user表中查询到一条用户信息。
用OAuth2.0去认证用户
当用户注册成功后,就可以登陆了,在用户登陆时,我们需要用OAuth2.0去认证他。
进入channel中,在prepary中初始化一个authServer:
class HeroesChannel extends ApplicationChannel {
ManagedContext context;
// Add this field
AuthServer authServer;
Future prepare() async {
logger.onRecord.listen((rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
final config = HeroConfig(options.configurationFilePath);
final dataModel = ManagedDataModel.fromCurrentMirrorSystem();
final persistentStore = PostgreSQLPersistentStore.fromConnectionInfo(
config.database.username,
config.database.password,
config.database.host,
config.database.port,
config.database.databaseName);
context = ManagedContext(dataModel, persistentStore);
// 添加这两行
final authStorage = ManagedAuthDelegate<User>(context);
authServer = AuthServer(authStorage);
}
...
那么是不是像注册时那样,我们需要自定义一个controller来实现登陆逻辑呢?答案是不需要的,Aqueduct里已经内置好了一个专业用于认证的controller,我们直接拿来用就好了,这就是标准化的好处。
@override
Controller get entryPoint {
final router = Router();
// 直接添加这个路由即可
router
.route('/auth/token')
.link(() => AuthController(authServer));
router
.route('/heroes/[:id]')
.link(() => HeroesController(context));
router
.route('/register')
.link(() => RegisterController(context, authServer));
return router;
}
然后向数据库中添加一个clientID:
aqueduct auth add-client --id com.heroes.tutorial --connect postgres://heroes_user:password@localhost:5432/heroes
接着来看一下OAuth2.0的请求协议有哪些内容:
- 请求头中包含clientID 和 [客户端密码];
- 请求体中包含用户名和密码;
- 请求体中包含验证类型,由于是自己的服务,所以我这里采用password类型;
- 必须用POST请求提交application/x-www-form-urlencoded格式。
请求token
执行:
curl -X POST http://localhost:8888/auth/token -H 'Authorization: Basic Y29tLmhlcm9lcy50dXRvcmlhbDo=' -H 'Content-Type: application/x-www-form-urlencoded' -d 'username=bob&password=password&grant_type=password'
如果返回如下信息,说明token请求成功:
{"access_token":"687PWKFHRTQ9MveQ2dKvP95D4cWie1gh","token_type":"bearer","expires_in":86399}
验证token
router
.route('/heroes/[:id]')
//加上验证路由
.link(() => Authorizer.bearer(authServer))
.link(() => HeroesController(context));
然后测试:
curl -X GET --verbose http://localhost:8888/heroes
直接访问会返回401 Unauthorized错误。那么我们加上token后再访问:
curl -X GET http://localhost:8888/heroes -H 'Authorization: Bearer 687PWKFHRTQ9MveQ2dKvP95D4cWie1gh'
没问题的话就能获取到英雄列表了。