Auth Controller
AuthController یک کلاس انتزاعی است که از Controller ارثبری میکند. این کلاس قرارداد (contract) احراز هویت و مجوزدهی را برای محافظت از مسیرها فراهم میکند. آن را یکبار برای هر اپلیکیشن پیادهسازی میکنید و به هر مسیری که نیاز به ورود (login) دارد ضمیمه میکنید.
متدهای انتزاعی
| متد | چه زمانی اجرا میشود | چه چیزی باید بازگردانده شود |
|---|---|---|
auth() |
در هر درخواست به یک مسیر محافظتشده | true برای اجازه دادن، false برای رد کردن (و مدیریت پاسخ) |
authApi() |
در درخواستها به endpointهای محافظتشده /api/ |
true برای اجازه دادن |
checkLogin() |
توسط auth() برای اعتبارسنجی session فراخوانی میشود |
یک record بهشکل ({bool success, String message, T? user}) |
checkPermission() |
پس از موفقیت auth()، route.permissions را بررسی میکند |
true اگر کاربر مجوز لازم را داشته باشد |
loginPost() |
مدیریت POST به مسیر فرم ورود | پاسخ HTML |
register() |
مدیریت ارسال فرم ثبتنام، در صورتی که اپلیکیشن شما از ثبتنام خودکار پشتیبانی کند | پاسخ HTML |
newUser() |
ایجاد یک حساب کاربری جدید (مثلاً از یک پنل مدیریت) | پاسخ HTML |
logout() |
مدیریت مسیر خروج (logout) | هدایت مجدد یا HTML |
updateAuth(email, password, user) |
پس از ورود موفق | ذخیره session/کوکی |
removeAuth() |
هنگام خروج | پاک کردن session/کوکی |
index() |
به ارثرسیده از Controller — handler پیشفرض اگر مسیر اکشن index/controller مخصوص به خودش را نداشته باشد |
هر چیزی که این مسیر باید نمایش دهد |
اگر اپلیکیشن شما نیازی به ثبتنام ندارد، معمول است که register()/newUser() را طوری بگذارید که UnimplementedError() پرتاب کنند، همانطور که پروژه مثال این کار را انجام میدهد — به مثال زیر مراجعه کنید.
مثال کامل
این 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 یک JSON 403 دریافت میکنند، درخواستهای وب یک هدایت مجدد دریافت میکنند
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);
}
}
ضمیمه کردن به یک مسیر
final authController = AppAuthController();
FinchRoute(
key: 'example.panel',
path: 'panel',
methods: Methods.ALL,
auth: authController,
permissions: ['admin'],
index: homeController.exampleAuth,
),
جریان درخواست برای یک مسیر محافظتشده:
- مسیر مطابقت پیدا میکند →
auth()فراخوانی میشود → اگرfalseبرگرداند، درخواست متوقف میشودcheckPermission()فراخوانی میشود → اگرfalseبرگرداند، درخواست متوقف میشود- handler کنترلر / index اجرا میشود
هدر Authorization
برای مسیرهای API، هدر Authorization را از طریق rq.authorization بخوانید:
var auth = rq.authorization;
if (auth.type == AuthType.bearer) {
String token = auth.value;
// اعتبارسنجی token
}
if (auth.type == AuthType.basic) {
String user = auth.getBasicUsername();
String pass = auth.getBasicPassword();
// اعتبارسنجی اعتبارنامهها (credentials)
}
AuthType همچنین مقادیر digest، hawk، aws و akamai را برای سازگاری با آینده (forward compatibility) تعریف میکند، اما امروز فقط basic و bearer واقعاً parse میشوند — هر scheme دیگری (یا یک هدر گمشده/نادرست) به AuthType.none resolve میشود.
Permissions
Permissions یک کلاس کوچک شامل ثابتهای از پیش تعریفشده رشتهای دسترسی (permission) است که میتوانید بهجای hardcode کردن رشتهها در همهجا از آن استفاده کنید:
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 را extend کنید) و آنها را به همان شکل درون checkPermission() بررسی کنید، همانطور که AppAuthController.checkPermission() پروژه مثال در بالا این کار را انجام میدهد.