<form> <input type="radio" name="sex" value="male">Male<br> <input type="radio" name="sex" value="female">Female </form>
- نصب پایتون 2.5 یا 2.6 یا 2.7 که فعلا در سایت آن، نسخهی 2.7 در دسترس هست. توجه داشته باشید که هنوز برای نسخهی 3 پایتون پشتیبانی صورت نگرفته است.
- آخرین نسخهی sdk را هم میتوانید از این آدرس به صورت zip و یا از این آدرس به صورت tar دانلود کنید و در صورتیکه دوست دارید به سورس آن دسترسی داشته باشید یا اینکه از سورسهای مشارکت شده یا غیر رسمی استفاده کنید، از این صفحه آن را دریافت کنید.
(C:\Users\aym\Downloads\addon-sdk-1.17) C:\Users\aym\Downloads\addon-sdk-1.17\bin>
source bin/activate
bash bin/activate
(addon-sdk)~/mozilla/addon-sdk >
آغاز به کار
mkdir fxaddon cd fxaddon cfx init
* lib directory created * data directory created * test directory created * doc directory created * README.md written * package.json written * test/test-main.js written * lib/main.js written * doc/main.md written Your sample add-on is now ready for testing: try "cfx test" and then "cfx run". Have fun!"
{ "name": "fxaddon", "title": "fxaddon", "id": "jid1-QfyqpNby9lTlcQ", "description": "a basic add-on", "author": "", "license": "MPL 2.0", "version": "0.1" }
{ "name": "dotnettips", "title": ".net Tips Updater", "id": "jid1-QfyqpNby9lTlcQ", "description": "This extension keeps you updated on current activities on dotnettips.info", "author": "yeganehaym@gmail.com", "license": "MPL 2.0", "version": "0.1" }
var button= require('sdk/ui/button/action');
buttons.ActionButton({...});
var tgbutton = require('sdk/ui/button/toggle'); var panels = require("sdk/panel"); var self = require("sdk/self"); var button = tgbutton.ToggleButton({ id: "updaterui", label: ".Net Updater", icon: { "16": "./icon-16.png", "32": "./icon-32.png", "64": "./icon-64.png" }, onChange: handleChange }); var panel = panels.Panel({ contentURL: self.data.url("./popup.html"), onHide: handleHide }); function handleChange(state) { if (state.checked) { panel.show({ position: button }); } } function handleHide() { button.state('window', {checked: false}); }
tgbutton.ToggleButton
require('sdk/ui/button/toggle').ToggleButton
Context Menus
var contextMenu = require("sdk/context-menu"); var home = contextMenu.Item({ label: "صفحه اصلی", data: "https://www.dntips.ir/" }); var postsarchive = contextMenu.Item({ label: "مطالب سایت", data: "https://www.dntips.ir/postsarchive" }); var menuItem = contextMenu.Menu({ label: "Open .Net Tips", context: contextMenu.PageContext(), items: [home, postsarchive], image: self.data.url("icon-16.png"), contentScript: 'self.on("click", function (node, data) {' + ' window.location.href = data;' + '});' });
SelectorContext("img")
SelectorContext("img,a[href]")
var tabs = require("sdk/tabs"); var menuItem = contextMenu.Menu({ label: "Open .Net Tips", context: contextMenu.PageContext(), items: [home, postsarchive], image: self.data.url("icon-16.png"), contentScript: 'self.on("click", function (node, data) {' + ' self.postMessage(data);' + '});', onMessage: function (data) { tabs.open(data); } });
var Url="https://www.dntips.ir/search?term="; var searchMenu = contextMenu.Item({ label: "search for", context: [contextMenu.PredicateContext(checkText),contextMenu.SelectionContext()], image: self.data.url("icon-16.png"), contentScript: 'self.on("click", function () {' + ' var text = window.getSelection().toString();' + ' if (text.length > 20)' + ' text = text.substr(0, 20);' + ' self.postMessage(text);'+ '})', onMessage: function (data) { tabs.open(Url+data); } }); function checkText(data) { if(data.selectionText === null) return false; console.log('selectionText: ' + data.selectionText); //handle showing or hiding of menu items based on the text content. menuItemToggle(data.selectionText); return true; }; function menuItemToggle(text){ var searchText="جست و جو برای "; searchMenu.label=searchText+text; };
در قسمت آینده موارد بیشتری را در مورد افزونه نویسی در فایرفاکس بررسی خواهیم کرد و افزونه را تکمیل خواهیم کرد
مدیریت سفارشی سطوح دسترسی کاربران در MVC
گفتید که مجوزها بعد از یک بار واکشی در کوکی ذخیره میشه، این به خاطر وجود مقدار true برای خصوصیت cacheRolesInCookie هست درسته؟ اما من یک RoleProvider سفارشی تهیه کردم که به صورت زیر اونو به وب کانفیگ اضافه کردم:
<roleManager cacheRolesInCookie="true" enabled="true" defaultProvider="CustomRoleProvider"> <providers> <clear /> <add name="CustomRoleProvider" type="MvcApp.UserInterface.Models.CustomRoleProvider" connectionStringName="MvcAppDb" applicationName="/" /> </providers>
</roleManager>
سوال بعدیم اینه که این نقش چطوری توی مرورگر کاربر ذخیره میشه؟ آیا رمز گزاری شده هست یا نه به صورت Plain Text ذخیره میشه؟ اگه رمزگزاری میشه با چه الگوریتمی این کار انجام میشه و آیا میشه اونو Decrypt کرد؟
با تشکر
- ViewUsers
- CreateUser
- EditUser
- DeleteUser
همانطور که مشاهده میکنید، المنتهایی در صفحه وجود دارند که کاربر X نباید آنها را مشاهده کند. از جمله دکمه حذف کاربر و دکمه ایجاد کاربر. برای مخفی کردن آنها چه راهحلی را میتوان ارائه داد؟ شاید بخواهید برای اینکار از ngIf* استفاده کنید. برای اینکار کافی است دست بکار شوید تا مشکلاتی را که در این روش به آنها بر میخورید، متوجه شوید. از جمله این مشکلات میتوان به پیچیدگی بسیار زیاد و وجود کدهای تکراری در هر کامپوننت اشاره کرد (در بهترین حالت هر کامپوننت باید سرویس حاوی دسترسیها را در خود تزریق کرده و با استفاده از توابعی، وجود یا عدم وجود دسترسی را بررسی کند).
راهحل بهتر، استفاده از یک Directive سفارشی است. همچنین ماژول Ng2Permission علاوه بر فراهم کردن Directive جهت مدیریت المنتهای روی صفحه، امکاناتی را جهت نگهداری و تعریف دسترسیهای جدید و همچنین محافظت از Routeها فراهم کرده است. این ماژول الهام گرفته از ماژول angular-permission میباشد.
کافی است با استفاده از دستور زیر این ماژول را نصب کنید:
npm install angular2-permission --save
بعد از نصب، ماژول Ng2Permission را در قسمت imports در ماژول اصلی برنامه، اضافه کنید.
import { Ng2Permission } from 'angular2-permission'; @NgModule({ imports: [ Ng2Permission ] })
مدیریت دسترسیها
توضیحات | امضاء |
تعریف دسترسیهای جدید | define(permissions: Array<string>): void |
افزودن دسترسی جدید | add(permission: string ) : void |
حذف دسترسی مشخص شده | remove(permission: string ) : void |
برسی اینکه دسترسی قبلا تعریف شده است؟ | hasDefined( permission : string ) : boolean |
برسی اینکه حداقل یکی از دسترسیهای ورودی قبلا تعریف شده است؟ | hasOneDefined(permissions: Array < string > ) : boolean |
حذف تمامی دسترسیها | clearStore( ) : void |
دریافت تمامی دسترسیهای تعریف شده | get store ( ) : Array < string> |
Emitter جهت تغییر در دسترسیها | get permissionStoreChangeEmitter ( ) : EventEmitter< an y> |
برای مثال جهت تعریف دسترسیهای جدید، کافی است سرویس PermissionService را تزریق کرده و با استفاده از متدهای define اقدام به اینکار کنید:
import { PermissionService } from 'angular2-permission'; @Component({ […] }) export class LoginComponent implements OnInit { constructor(private _permissionService: PermissionService) { this._permissionService.define(['ViewUsers', 'CreateUser', 'EditUser', 'DeleteUser']); } }
آنچه که واضح است، این است که لیست دسترسیها میتوانند از سمت سرور تامین شوند.
محافظت از مسیرهای تعریف شده
import { PermissionGuard, IPermissionGuardModel } from 'angular2-permission'; […] const routes: Routes = [ { path: 'login', component: LoginComponent, children: [] }, { path: 'users', component: UserListComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['ViewUsers'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: '403', component: AccessDeniedComponent, children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
توضیحات | خصوصیت |
فقط دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Only |
تمامی دسترسیهای تعریف شده، به جز دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Except |
در صورتیکه تقاضای غیر مجازی جهت پیمایش به این مسیر صادر شد، کاربر را به این مسیر هدایت میکند. | RedirectTo |
{ path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['Admin', 'CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }
محافظت از المنتهای صفحه
توضیحات | Input type | Directive |
فقط دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Arrayy<string> | hasPermission |
تمامی دسترسیهای موجود، به جز دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Array<string> | exceptPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت دارد. | string | Function | onAuthorizedPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت را ندارد. | string | Function | onUnauthorizedPermission |
<button type="button" [hasPermission]="['DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
در صورتیکه بیش از یک دسترسی مد نظر باشد، با کاما از هم جدا خواهند شد.
<button type="button" [hasPermission]="[ 'Admin', 'DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [exceptPermission]="['GeustUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [hasPermission]="['GeustUser']" onAuthorizedPermission="enable" onUnauthorizedPermission="disable"> <span aria-hidden="true"></span> Delete </button>
رفتار | مقدار |
حذف خصوصیت disabled از المنت | enable |
افزودن خصوصیت disabled به المنت | disable |
تنظیم استایل display به inherit | show |
تنظیم استایل display به none | hide |
@Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent { constructor() { } OnAuthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility ="inherit"; } OnUnauthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility = "hidden"; } }
<button [hasPermission]="['CreateUser']" [onAuthorizedPermission]="OnAuthorizedPermission" [onUnauthorizedPermission]="OnUnauthorizedPermission"> <span aria-hidden="true"></span> Add New User </button>
• NotNull/NotEmpty
• Matches (regex)
• InclusiveBetween (range)
• CreditCard
• EqualTo (cross-property equality comparison)
• MaxLength
• MinLength
• Length
اما باید دقت داشت که سایر قابلیتهای این کتابخانه، قابلیت ترجمهی به قواعد unobtrusive java script validation ندارند؛ مانند:
- استفادهی از شرطها توسط متدهای When و یا Unless (برای مثال اگر خاصیت A دارای مقدار 5 بود، آنگاه ورود اطلاعات خاصیت B باید اجباری شود)
- تعریف قواعد اعتبارسنجی سفارشی، برای مثال توسط متد Must
- تعریف RuleSetها (برای مثال تعیین کنید که از یک مدل، فقط تعدادی از خواص آن اعتبارسنجی شوند و نه تمام آنها)
در یک چنین حالتهایی باید کاربر اطلاعات کامل فرم را به سمت سرور ارسال کند و در post-back صورت گرفته، نتایج اعتبارسنجی را دریافت کند.
روش توسعهی اعتبارسنجی سمت کلاینت کتابخانهی FluentValidation جهت سازگاری آن با unobtrusive java script validation
باتوجه به اینکه قواعد سفارشی کتابخانهی FluentValidation، معادل unobtrusive java script validation ای ندارند، در ادامه مثالی را جهت تدارک آنها بررسی میکنیم. برای این منظور، معادل مطلب «ایجاد ویژگیهای اعتبارسنجی سفارشی در ASP.NET Core 3.1 به همراه اعتبارسنجی سمت کلاینت آنها» را بر اساس ویژگیهای کتابخانهی FluentValidation پیاده سازی میکنیم.
1) ایجاد اعتبارسنج سمت سرور
میخواهیم اگر مقدار عددی خاصیتی بیشتر از مقدار عددی خاصیت دیگری بود، اعتبارسنجی آن با شکست مواجه شود. به همین منظور با پیاده سازی یک PropertyValidator سفارشی، LowerThanValidator را به صورت زیر تعریف میکنیم:
using System; using FluentValidation; using FluentValidation.Validators; namespace FluentValidationSample.Models { public class LowerThanValidator : PropertyValidator { public string DependentProperty { get; set; } public LowerThanValidator(string dependentProperty) : base($"باید کمتر از {dependentProperty} باشد") { DependentProperty = dependentProperty; } protected override bool IsValid(PropertyValidatorContext context) { if (context.PropertyValue == null) { return false; } var typeInfo = context.Instance.GetType(); var dependentPropertyValue = Convert.ToInt32(typeInfo.GetProperty(DependentProperty).GetValue(context.Instance, null)); return int.Parse(context.PropertyValue.ToString()) < dependentPropertyValue; } } public static class CustomFluentValidationExtensions { public static IRuleBuilderOptions<T, int> LowerThan<T>( this IRuleBuilder<T, int> ruleBuilder, string dependentProperty) { return ruleBuilder.SetValidator(new LowerThanValidator(dependentProperty)); } } }
- همچنین جهت سهولت کار فراخوانی آن، یک متد الحاقی جدید به نام LowerThan نیز در اینجا تعریف شدهاست.
- البته اگر قصد اعتبارسنجی سمت سرور را ندارید، میتوان در متد IsValid، مقدار true را بازگشت داد.
2) ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور
در ادامه نیاز است ویژگیهای data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation; using FluentValidation.AspNetCore; using FluentValidation.Internal; using FluentValidation.Resources; using FluentValidation.Validators; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; namespace FluentValidationSample.Models { public class LowerThanClientValidator : ClientValidatorBase { private LowerThanValidator LowerThanValidator { get { return (LowerThanValidator)Validator; } } public LowerThanClientValidator(PropertyRule rule, IPropertyValidator validator) : base(rule, validator) { } public override void AddValidation(ClientModelValidationContext context) { MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-LowerThan", GetErrorMessage(context)); MergeAttribute(context.Attributes, "data-val-LowerThan-dependentproperty", LowerThanValidator.DependentProperty); } private string GetErrorMessage(ClientModelValidationContext context) { var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName()); string messageTemplate; try { messageTemplate = Validator.Options.ErrorMessageSource.GetString(null); } catch (FluentValidationMessageFormatException) { messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>(); } return formatter.BuildMessage(messageTemplate); } } }
<input dir="rtl" type="number" data-val="true" data-val-lowerthan="باید کمتر از Age باشد" data-val-lowerthan-dependentproperty="Age" data-val-required="'سابقه کار' must not be empty." id="Experience" name="Experience" value="">
3) تعریف مدل کاربران و اعتبارسنجی آن
مدلی که در این مثال از آن استفاده شده، یک چنین تعریفی را دارد و در قسمت اعتبارسنجی خاصیت Experience آن، از متد الحاقی جدید LowerThan استفاده شدهاست:
public class UserModel { [Display(Name = "نام کاربری")] public string Username { get; set; } [Display(Name = "سن")] public int Age { get; set; } [Display(Name = "سابقه کار")] public int Experience { get; set; } } public class UserValidator : AbstractValidator<UserModel> { public UserValidator() { RuleFor(x => x.Username).NotNull(); RuleFor(x => x.Age).NotNull(); RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull(); } }
4) افزودن اعتبارسنجهای تعریف شده به تنظیمات برنامه
پس از تعریف LowerThanValidator و LowerThanClientValidator، روش افزودن آنها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly()); fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; fv.ConfigureClientsideValidation(clientSideValidation => { clientSideValidation.Add( validatorType: typeof(LowerThanValidator), factory: (context, rule, validator) => new LowerThanClientValidator(rule, validator)); }); } ); }
5) تعریف کدهای جاوا اسکریپتی مورد نیاز
پیش از هرکاری، اسکریپتهای فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/lib/jquery-validation/dist/jquery.validate.js"></script> <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script> <script src="~/js/site.js" asp-append-version="true"></script>
$.validator.unobtrusive.adapters.add('LowerThan', ['dependentproperty'], function (options) { options.rules['LowerThan'] = { dependentproperty: options.params['dependentproperty'] }; options.messages['LowerThan'] = options.message; }); $.validator.addMethod('LowerThan', function (value, element, parameters) { var dependentProperty = '#' + parameters['dependentproperty']; var dependentControl = $(dependentProperty); if (dependentControl) { var targetvalue = dependentControl.val(); if (parseInt(targetvalue) > parseInt(value)) { return true; } return false; } return true; });
6) آزمایش برنامه
@using FluentValidationSample.Models @model UserModel @{ ViewData["Title"] = "Home Page"; } <div dir="rtl"> <form asp-controller="Home" asp-action="RegisterUser" method="post"> <fieldset class="form-group"> <legend>ثبت نام</legend> <div class="form-group row"> <label asp-for="Username" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Username" class="form-control" /> <span asp-validation-for="Username" class="text-danger"></span> </div> </div> <div class="form-group row"> <label asp-for="Age" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Age" class="form-control" /> <span asp-validation-for="Age" class="text-danger"></span> </div> </div> <div class="form-group row"> <label asp-for="Experience" class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10"> <input dir="rtl" asp-for="Experience" class="form-control" /> <span asp-validation-for="Experience" class="text-danger"></span> </div> </div> <div class="form-group row"> <label class="col-md-2 col-form-label text-md-left"></label> <div class="col-md-10 text-md-right"> <button type="submit" class="btn btn-info col-md-2">ارسال</button> </div> </div> </fieldset> </form> </div>
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
عدم authorization بر اساس Permissions
جدول Roles
برای مثال در Views\Shared\_LoginPartial.cshtml
کنترل دسترسی بر اساس permission
کنترل دسترسی بر اسای Role
اما در پروژه بنده کنترل دسترسی بر اساس permission کار نمیکند.
رسم نمودار توسط Kendo Chart
<div id="chart" style="direction: ltr"> </div>
public class Form { public string Name { get; set; } public string Title { get; set; } public List<BaseElement> Elements { get; set; } } public abstract class BaseElement { public string Name { get; set; } public string Title { get; set; } } public class Section : BaseElement { public List<TextBox> Elements { get; set; } } public class TextBox : BaseElement { public string Value { get; set; } public string CssClass { get; set; } }
public class FormBuilderController : Controller { // // GET: /FormBuilder/ public ActionResult Index() { var form = new Form(); var section = new Section() { Title = "Basic Info", Name = "section01" }; section.Elements.Add(new TextBox() { Name = "txt1", Title = "First Text Box" }); form.Elements.Add(new TextBox() { Name = "txt1", Title = "Second Text Box" }); var formJson=JsonConvert.SerializeObject(form); return View(formJson); } }
<script type="text/ng-template" id="ElementTemplate"> <div ng-if="control.Type == 'JbSection'"> <h2>{{control.Title}}</h2> <ul> <li ng-repeat="control in control.Elements" ng-include="'ElementTemplate'"></li> </ul> </div> </script>
<script type="text/ng-template" id="element.html"> {{data.label}} <ul> <li ng-repeat="element in data.elements" ng-include="'element.html'"></li> </ul> </script> <ul ng-controller="NestedFormCtrl"> <li ng-repeat="field in formData" ng-include="'element.html'"></li> </ul>
ASP.NET MVC #7
@if (User.Identity.IsAuthenticated && User.IsInRole("Administrator")) { <div id="sidebar"> data </div> }