12/08/2018, 14:29

ASP.NET 5 và AngularJS Phần 6, An ninh

Đây là phần thứ 6 trong phần blog của loạt bài xây dựng ASP.NET 5 (ASP.NET vNext) ứng với AngularJS. Trong loạt bài đăng trên blog, tôi sẽ cho các bạn thấy làm thế nào để có thể tạo ra một ứng dụng Movie đơn giản sử dụng ASP.NET 5, MVC 6, và AngularJS. Bạn có thể tải về mã đã thảo luận trong bài ...

Đây là phần thứ 6 trong phần blog của loạt bài xây dựng ASP.NET 5 (ASP.NET vNext) ứng với AngularJS. Trong loạt bài đăng trên blog, tôi sẽ cho các bạn thấy làm thế nào để có thể tạo ra một ứng dụng Movie đơn giản sử dụng ASP.NET 5, MVC 6, và AngularJS.

Bạn có thể tải về mã đã thảo luận trong bài viết blog này từ GitHub: github.com/StephenWalther/MovieAngularJSApp

Trong bài viết trên blog này, tôi giải thích làm thế nào bạn có thể sử dụng ASP.NET Identityđể cung cấp quyền truy cập khác nhau cho người dùng khác nhau. Trong đó, tôi chứng minh làm thế nào để tạo ra hai người dùng tên là Stephen và Bob. Stephen sẽ có quyền chỉnh sửa trên cơ sở dữ liệu phim và Bob sẽ không. Dưới đây là những gì màn hình của Stephen trông như: alt

Chú ý rằng Stephen có thể chỉnh sửa, xóa, và thêm phim. Bây giờ, đây là những gì màn hình của Bob trông giống như: alt

Bob không thể làm bất cứ điều gì ngoại trừ xem danh sách các phim. Bob không được phép chỉnh sửa hoặc thêm phim.

kích hoạt ASP.NET Identity

Bước đầu tiên là kéo tất cả các gói NuGet cần phải sử dụng ASP.NET Identity. Bạn cần phải thêm các gói sau vào phần phụ thuộc của các tập tin project.json của bạn:

"dependencies": {
  "EntityFramework.SqlServer": "7.0.0-beta2",
  "EntityFramework.Commands": "7.0.0-beta2",
  "Microsoft.AspNet.Mvc": "6.0.0-beta2",
  /* "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-beta2", */
  "Microsoft.AspNet.Diagnostics": "1.0.0-beta2",
  "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta2",
  "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta2",
  "Microsoft.AspNet.Security.Cookies": "1.0.0-beta2",
  "Microsoft.AspNet.Server.IIS": "1.0.0-beta2",
  "Microsoft.AspNet.Server.WebListener": "1.0.0-beta2",
  "Microsoft.AspNet.StaticFiles": "1.0.0-beta2",
  "Microsoft.Framework.ConfigurationModel.Json": "1.0.0-beta2",
  "Microsoft.Framework.CodeGenerators.Mvc": "1.0.0-beta2",
  "Microsoft.Framework.Logging": "1.0.0-beta2",
  "Microsoft.Framework.Logging.Console": "1.0.0-beta2",
  "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta1"
},

Chú ý rằng tôi đã bao gồm các gói Microsoft.AspNet.Identity.EntityFramework và Microsoft.AspNet.Security.Cookies.Những gói trên được kéo trong phụ thuộc bổ sung như Microsoft.AspNet.Identity và Microsoft.AspNet.Security. Tiếp theo, bạn cần phải cập nhật ConfigureServices() trong tập tin Startup.cs của bạn. method ConfigureServices() được sử dụng để đăng ký dịch vụ với built-in Dependency Injection framework bao gồm với ASP.NET 5:

public void ConfigureServices(IServiceCollection services)
{
    // add Entity Framework
    services.AddEntityFramework(Configuration)
            .AddSqlServer()
            .AddDbContext<MoviesAppContext>(options =>
            {
                options.UseSqlServer(Configuration.Get("Data:DefaultConnection:ConnectionString"));
            });

    // add ASP.NET Identity
    services.AddIdentity<ApplicationUser, IdentityRole>(Configuration)
        .AddEntityFrameworkStores<MoviesAppContext>();

    // add ASP.NET MVC
    services.AddMvc();
}

Chú ý rằng tôi bổ sung thêm dịch vụ Identity. Khi tôi đăng ký dịch vụ, tôi xác định rằng tôi sẽ được sử dụng dịch vụ Identity với Entity Framework như lưu trữ dữ liệu của tôi để lưu trữ tên người dùng và mật khẩu. Nếu tôi đã chạy ứng dụng của tôi bên ngoài của Windows - ví dụ, sử dụng OSX - sau đó tôi có thể sử dụng một data store thay thế như SQLite. Xem URL sau đây để xem danh sách các nhà cung cấp rằng Microsoft có kế hoạch để hỗ trợ với Entity Framework 7: https://github.com/aspnet/EntityFramework/wiki/Using-EF7-in-Traditional-.NET-Applications

Cuối cùng, chúng ta cần phải cập method Configure() trong file Startup.cs. Configure () - không giống như method ConfigureServices () - được sử dụng bởi OWIN để cấu hình các ứng dụng pipeline. Dưới đây là những gì method Configure() được cập nhật:

public void Configure(IApplicationBuilder app)
{
    app.UseIdentity();
    app.UseMvc();

    CreateSampleData(app.ApplicationServices).Wait();
}

Chú ý tôi gọi app.UseIdentity () để thêm ASP.NET tới application pipeline. Tôi giải thích method CreateSampleData () bên dưới.

Sửa đổi DbContext

Để tận dụng lợi thế của ASP.NET Identity, chúng ta cần phải chỉnh sửa lớp DbContext của chúng ta. Dưới đây là MoviesAppContext :

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Data.Entity;
using System;

namespace MovieAngularJSApp.Models
{
    public class ApplicationUser : IdentityUser {}

    public class MoviesAppContext : IdentityDbContext<ApplicationUser>
    {
        public DbSet<Movie> Movies { get; set; }
    }
}

Chú ý rằng MoviesAppContext giờ đây thừa hưởng từ base IdentityDbContext. Kế thừa từ lớp cơ sở này sẽ thêm tất cả các thực thể bổ sung cần thiết cho Entity Framework. Nếu bạn mở cơ sở dữ liệu của bạn thì bạn có thể xem tất cả các bảng phụ mà bạn nhận được sau khi kế thừa từ IdentityDbContext:

alt

Chú ý rằng tôi nhận được bảng cơ sở dữ liệu bổ sung như các bảng AspNetUsers và AspNetUserClaims. Thêm users tại Startup Chúng tôi muốn thêm người dùng Stephen và Bob đến cơ sở dữ liệu của chúng tôi tự động lúc khởi động. Vì vậy, tôi sẽ sửa đổi các tập tin Startup.cs để nó tạo ra cơ sở dữ liệu và thêm dữ liệu mẫu tự động:

public void Configure(IApplicationBuilder app)
{
    app.UseIdentity();
    app.UseMvc();

    CreateSampleData(app.ApplicationServices).Wait();
}

private static async Task CreateSampleData(IServiceProvider applicationServices)
{
    using (var dbContext = applicationServices.GetService<MoviesAppContext>())
    {
        var sqlServerDatabase = dbContext.Database as SqlServerDatabase;
        if (sqlServerDatabase != null)
        {
            // Create database in user root (c:usersyour name)
            if (await sqlServerDatabase.EnsureCreatedAsync()) {
                // add some movies
                var movies = new List<Movie>
                {
                    new Movie {Title="Star Wars", Director="Lucas"},
                    new Movie {Title="King Kong", Director="Jackson"},
                    new Movie {Title="Memento", Director="Nolan"}
                };
                movies.ForEach(m => dbContext.Movies.AddAsync(m));

                // add some users
                var userManager = applicationServices.GetService<UserManager<ApplicationUser>>();

                // add editor user
                var stephen = new ApplicationUser
                {
                    UserName = "Stephen"
                };
                var result = await userManager.CreateAsync(stephen, "P@ssw0rd");
                await userManager.AddClaimAsync(stephen, new Claim("CanEdit", "true"));

                // add normal user
                var bob = new ApplicationUser
                {
                    UserName = "Bob"
                };
                await userManager.CreateAsync(bob, "P@ssw0rd");
            }

        }
    }
}

Chú ý rằng tôi tạo ra một async static method tên CreateSampeData (). Tôi cần phải thực hiện CreateSampleData () async vì method ASP.NET identity là async. method CreateSampleData () đầu tiên đảm bảo rằng MoviesDatabase tồn tại. Nếu không, các method EnsureCreatedAsync () tạo ra nó. Tiếp theo, ba bộ phim được thêm vào bảng cơ sở dữ liệu phim. Mỗi bộ phim được thêm vào bằng cách gọi method dbContext.Movies.AddAsync (). Cuối cùng, hai người được bổ sung vào cơ sở dữ liệu: Stephen và Bob. User được thêm vào bằng cách gọi method UserManager.CreateAsync (). Một claim được thêm cho user Stephen. Stephen được cung cấp quyền CanEdit. Chúng tôi sẽ sử dụng claim này để cho phép Stephen, nhưng không phải Bob,quyền chỉnh sửa phim. Bất cứ khi nào bạn muốn tạo dữ liệu mẫu, bạn chỉ có thể xóa các cơ sở dữ liệu MoviesDatabase. Trong Visual Studio, mở SQL Server cửa sổ Object Explorer, bấm chuột phải vào cơ sở dữ liệu của bạn, và chọn tùy chọn menu Delete. Hãy chắc chắn rằng bạn check Close existing connections hoặc bạn sẽ bị khóa từ database đang xóa.

alt

Bắt buộc người sử dụng đăng nhập

Bây giờ chúng ta sẽ có các thiết lập cơ bản định hướng, chúng ta có thể buộc người dùng phải đăng nhập bằng cách thêm một [Authorize] thuộc tính để điều khiển Trang chủ của chúng tôi như thế này:

using Microsoft.AspNet.Mvc;
using System.Security.Claims;

namespace MovieAngularJSApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {

        public IActionResult Index()
        {
        var user = (ClaimsIdentity)User.Identity;
        ViewBag.Name = user.Name;
        ViewBag.CanEdit = user.FindFirst("CanEdit") != null ? "true" : "false";
        return View();
        }
    }
}

Nếu một người dùng vô danh cố gắng để điều hướng đến thư mục gốc của ứng dụng của chúng tôi sau đó người dùng vô danh sẽ được chuyển hướng đến /account/login controller action. Dưới đây là những gì methods Login() trông như thế nào trong AccountController:

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;
using MovieAngularJSApp.Models;
using System.Threading.Tasks;

namespace MovieAngularJSApp.Controllers
{
    public class AccountController : Controller
    {
        private UserManager<ApplicationUser> _userManager;
        private SignInManager<ApplicationUser> _signInManager;

        public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
        }

        public IActionResult Login()
        {
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel login, string returnUrl = null)
        {
            var signInStatus = await _signInManager.PasswordSignInAsync(login.UserName, login.Password, false, false);
            if (signInStatus == SignInStatus.Success)
            {
                return Redirect("/home");
            }
            ModelState.AddModelError("", "Invalid username or password.");
            return View();
        }

        public IActionResult SignOut()
        {
            _signInManager.SignOut();
            return Redirect("/home");
        }

    }
}

Các lớp UserManager và SignInManager được truyền cho AccountController thông qua built-in ASP.NET 5 dependency injection.. method Login() gọi SignInManager.PasswordSignInAsync () để xác thực username và mật khẩu nhập vào bởi người sử dụng. Nếu sự kết hợp xác nhận sau đó đúng người dùng sẽ được chuyển hướng trở lại Home controller..

Truyền Claims Data đến AngularJS

Stephen có thể chỉnh sửa phim nhưng không phải Bob. Để làm công việc này, chúng tôi cần phải truyền claims data liên quan đến Stephen và Bob từ máy chủ (ASP.NET) cho client (AngularJS). Trong Home controller, chúng ta thêm claim CanEdit đến ViewBag như thế này:

using Microsoft.AspNet.Mvc;
using System.Security.Claims;

namespace MovieAngularJSApp.Controllers
{
    [Authorize]
    public class HomeController : Controller
    {

        public IActionResult Index()
        {
        var user = (ClaimsIdentity)User.Identity;
        ViewBag.Name = user.Name;
        ViewBag.CanEdit = user.FindFirst("CanEdit") != null ? "true" : "false";
        return View();
        }
    }
}

Chúng tôi cast Identity người dùng như một ClaimsIdentity và sau đó chúng ta gán giá trị của claim CanEdit đến property ViewBag.CanEdit. Bởi vì Stephen có Claim CanEdit, property ViewBag.CanEdit sẽ có giá trị "true" (chúng ta cần phải sử dụng một chuỗi ở đây vì Boolean.ToString () được chuyển đổi sang "True" thay vì "true" cái mà JavaScript không hiểu). Dưới đây là những gì Index view trông như:

<!DOCTYPE html>
<html ng-app="moviesApp">
<head>
    <base href="/">
    <meta charset="utf-8" />
    <title>Movies</title>

     
    <script src="//code.jquery.com/jquery-1.11.2.min.js"></script>

     
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-resource.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.3.8/angular-route.js"></script>

     
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap-theme.min.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.12.0/ui-bootstrap-tpls.min.js"></script>

     
    <link rel="stylesheet" href="/styles.css" />
    <script src="/app.js"></script>

     
    <script>
        angular.module("moviesApp").value("canEdit", @ViewBag.CanEdit);
    </script>

</head>
<body ng-cloak>

    @ViewBag.Name / <a href="/account/signout" target="_self">sign out</a>

    <div class="container-fluid">
        <ng-view></ng-view>
    </div>
</body>
</html>

Chú ý đến mục của trang có gắn nhãn Get Server Data. Giá trị của claim CanEdit được gán cho một giá trị mô-đun AngularJS tên "canEdit". Giá trị canEdit được sử dụng trong AngularJS moviesListController như thế này:

/* Movies List Controller  */
MoviesListController.$inject = ['$scope', 'Movie', 'canEdit'];

function MoviesListController($scope, Movie, canEdit) {
    $scope.canEdit = canEdit;
    $scope.movies = Movie.query();
}

Giá trị canEdit được đua vào bộ điều khiển và gắn vào scope của bộ điều khiển. Điều này làm cho các giá trị canEdit có sẵn trong AngularJS list view được sử dụng để hiển thị danh sách các phim:

<h1>List Movies</h1>
    <table class="table table-bordered table-striped">
        <thead>
            <tr>
                <th ng-show="canEdit"></th>
                <th>Title</th>
                <th>Director</th>
            </tr>
        </thead>
        <tbody>
            <tr ng-repeat="movie in movies">
                <td ng-show="canEdit">
                    <a href="/movies/edit/{{movie.Id}}" class="btn btn-default btn-xs">edit</a>
                    <a href="/movies/delete/{{movie.Id}}" class="btn btn-danger btn-xs">delete</a>
                </td>
                <td>{{movie.Title}}</td>
                <td>{{movie.Director}}</td>
            </tr>
        </tbody>
    </table>

    <p ng-show="canEdit">
        <a href="/movies/add" class="btn btn-primary">Add New Movie</a>
    </p>
</div>

Chú ý rằng AngularJS ng-show directive được sử dụng hai lần trong list view. ng-show directiveđược sử dụng để ẩn hoặc hiển thị các cột đầu tiên của bảng HTML để liệt kê các phim (ẩn hoặc hiển thị các nút chỉnh sửa và xóa). ng-show directive cũng được sử dụng để ẩn hoặc hiển thị các nút Add New Movie ở dưới cùng của giao diện. Bởi vì Stephen claim CanEdit, khi Stephen mở xem danh sách, Stephen thấy các nút chỉnh sửa, xóa, và thêm:

alt

Tuy nhiên, Bob không có claim CanEdit. Vì vậy, Bob không có các nút:

alt

Bảo mật API Controller Actions

Có một điều cuối cùng mà chúng ta cần phải làm để cho tất cả các thứ chạy. Bob không thể nhìn thấy các nút chỉnh sửa nhưng Bob có thể, nếu anh ta lén lút, bỏ qua các view và trực tiếp tới Movies API controller. Đây là cách chúng ta ngăn chặn điều này xảy ra:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc;
using MovieAngularJSApp.Models;
using System.Security.Claims;

namespace MovieAngularJSApp.API.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
        private readonly MoviesAppContext _dbContext;

        public MoviesController(MoviesAppContext dbContext)
        {
            _dbContext = dbContext;
        }

        [HttpGet]
        public IEnumerable<Movie> Get()
        {
            return _dbContext.Movies;
        }

        [HttpGet("{id:int}")]
        public IActionResult Get(int id)
        {
            var movie = _dbContext.Movies.FirstOrDefault(m => m.Id == id);
            if (movie == null) {
                return new HttpNotFoundResult();
            } else {
                return new ObjectResult(movie);
            }
        }

        [HttpPost]
        [Authorize("CanEdit", "true")]
        public IActionResult Post([FromBody]Movie movie)
               
0