Auth Controller

AuthController 是一个继承自 Controller 的抽象类。它为保护路由提供了身份验证与授权的契约。你只需在应用中实现一次,然后将其附加到任何需要登录才能访问的路由上。

Abstract Methods

方法 运行时机 应返回的内容
auth() 每次请求受保护路由时 返回 true 表示允许,返回 false 表示拒绝(并自行处理响应)
authApi() 请求受保护的 /api/ 端点时 返回 true 表示允许
checkLogin() 由 auth() 调用,用于验证 session 一个记录 ({bool success, String message, T? user})
checkPermission() auth() 通过之后,检查 route.permissions 如果用户拥有所需权限则返回 true
loginPost() 处理登录表单路由的 POST 请求 HTML 响应
register() 处理注册表单的提交(如果你的应用支持自助注册) HTML 响应
newUser() 创建新用户账号(例如从后台管理面板创建) HTML 响应
logout() 处理登出路由 重定向或 HTML
updateAuth(email, password, user) 登录成功之后 存储 session/cookie
removeAuth() 登出时 清除 session/cookie
index() 继承自 Controller——当该路由自身没有设置 index/controller 操作时使用的默认处理程序 该路由应显示的内容

如果你的应用不需要注册功能,通常的做法是让 register()/newUser() 直接抛出 UnimplementedError(),就像示例项目所做的那样——参见下方的示例。

Complete Example

这是来自 Finch 示例项目的 auth controller:

import 'package:finch/finch_route.dart';
import 'package:finch/finch_tools.dart';
import '../models/mock_user_model.dart';
import '../route/web_route.dart';

class AppAuthController extends AuthController<MockUserModel> {
  MockUserModel? userLogined;

  @override
  Future<bool> auth() async {
    var res = await checkLogin();
    if (!res.success) {
      // API requests get a JSON 403, web requests get a redirect
      if (rq.isApiEndpoint) {
        await rq.renderError(
          403,
          toData: true,
          params: {'message': 'Please login.', 'success': false},
        );
      } else {
        await rq.redirect('/example/form');
      }
      return false;
    }
    updateAuth(res.user!.email, res.user!.password, res.user!);
    return true;
  }

  @override
  Future<bool> authApi() async {
    var auth = rq.authorization;
    var mockUser = MockUserModel();

    if (auth.type == AuthType.basic) {
      String email = auth.getBasicUsername();
      String password = auth.getBasicPassword();
      return email == mockUser.email && password == mockUser.password;
    } else if (auth.type == AuthType.bearer) {
      return auth.value == '${mockUser.email} ${mockUser.password}';
    }
    return false;
  }

  @override
  Future<({bool success, String message, MockUserModel? user})>
      checkLogin() async {
    var mockUser = MockUserModel();
    var userSession = rq.getSession('user', def: '');

    if (userSession == mockUser.email) {
      return (success: true, message: 'Success.', user: mockUser);
    }
    return (success: false, message: 'Please login.', user: mockUser);
  }

  @override
  Future<bool> checkPermission() async {
    if (rq.route == null || userLogined == null) return false;

    var permission = userLogined!.permission;
    if (rq.route!.permissions.isNotEmpty &&
        !rq.route!.permissions.contains(permission)) {
      return false;
    }
    return true;
  }

  @override
  Future<String> loginPost() async {
    var formLogin = LoginForm();
    await formLogin.check(
      onInvalid: (p0) {},
      onValid: (p0) {
        var mockUser = MockUserModel();
        var email = formLogin.get<String>('email', def: '');
        var password = formLogin.get<String>('password', def: '');
        if (email == mockUser.email && password == mockUser.password) {
          updateAuth(email, password, mockUser);
        } else {
          rq.addParam('errorLogin', 'form.validation.loginError'.tr);
        }
      },
    );
    return homeController.renderView('example/form');
  }

  @override
  Future<String> logout() {
    removeAuth();
    return rq.redirect('/example/form');
  }

  @override
  Future<String> newUser() => throw UnimplementedError();

  @override
  Future<String> register() => throw UnimplementedError();

  @override
  void removeAuth() {
    rq.session.remove('user');
    rq.removeCookie('user');
    userLogined = null;
  }

  @override
  void updateAuth(String email, String password, MockUserModel user) {
    userLogined = user;
    rq.addSession('user', email);
  }
}

Attaching to a Route

final authController = AppAuthController();

FinchRoute(
  key: 'example.panel',
  path: 'panel',
  methods: Methods.ALL,
  auth: authController,
  permissions: ['admin'],
  index: homeController.exampleAuth,
),

受保护路由的请求流程:

  1. 路由匹配 →
  2. 调用 auth() → 若返回 false,请求停止
  3. 调用 checkPermission() → 若返回 false,请求停止
  4. 执行控制器 / index 处理程序

Authorization Header

对于 API 路由,可以通过 rq.authorization 读取 Authorization 请求头:

var auth = rq.authorization;

if (auth.type == AuthType.bearer) {
  String token = auth.value;
  // validate token
}

if (auth.type == AuthType.basic) {
  String user = auth.getBasicUsername();
  String pass = auth.getBasicPassword();
  // validate credentials
}

AuthType 还为了向前兼容而声明了 digest、hawk、aws 和 akamai 这几个值,但目前实际上只解析 basic 和 bearer——任何其他方案(或缺失/格式错误的请求头)都会解析为 AuthType.none。

Permissions

Permissions 是一个小型类,包含预定义的权限字符串常量,供你使用,而无需在各处硬编码字符串:

class Permissions {
  static final String none = 'none';
  static final String superAdmin = 'super-admin';
}
FinchRoute(
  key: 'admin.panel',
  path: 'admin/panel',
  auth: authController,
  permissions: [Permissions.superAdmin],
  index: adminController.panel,
),

它只定义了这两个常量——对于 none/super-admin 之外的其他权限,请自行定义权限字符串(或扩展 Permissions),并在 checkPermission() 内部以同样的方式检查它们,就像上面示例项目中的 AppAuthController.checkPermission() 所做的那样。