你的位置:首页 > ASP.net教程

[ASP.net教程]购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API


 

chsakell分享了前端使用AngularJS,后端使用ASP.NET Web API的购物车案例,非常精彩,这里这里记录下对此项目的理解。


文章:
http://chsakell.com/2015/01/31/angularjs-feat-web-api/
http://chsakell.com/2015/03/07/angularjs-feat-web-api-enable-session-state/

 

源码:
https://github.com/chsakell/webapiangularjssecurity

 

 

本系列共三篇,本篇是第三篇。


购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(1)--后端
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(2)--前端,以及前后端Session
购物车Demo,前端使用AngularJS,后端使用ASP.NET Web API(3)--Idetity,OWIN前后端验证

 

这里会涉及到三方面的内容:

 

1、ASP.NET Identity & Entity Framework

● Identity User
● User Mnager

 

2、OWIN Middleware

● Authorization Server
● Bearer Auhentication

 

3、AngularJS

● Generate Tokens
● Creae authorized requests


1、ASP.NET Identity & Entity Framework

 

首先安装Microsoft ASP.NET Identity EntityFramework。

 

添加一个有关用户的领域模型,继承IdentityUser。

 

public class AppStoreUser : IdentityUser{  ...}

 

配置用户,继承EntityTypeConfiguration<T>

 

public class AppStoreUserConfiguraiton : EntityTypeConfiguration<AppStoreUser>{  public AppStoreUserConfiguration()  {    ToTable("Users");  }}

 

然后让上下文继承Identity特有的上下文类。

 

public class StoreContext : IdentityDbContext<AppStoreUser>{  public StoreContext() : base("StoreContext", thrwoIfVISchema: false)  {    protected override void OnModelCreating(DbModelBuilder modelBuilder)    {      modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);      modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);      modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });      modelBuilder.Configurations.Add(new AppStoreUserConfiguration());      modelBuilder.Configurations.Add(new CategoryConfiguration());      modelBuilder.Configurations.Add(new OrderConfiguration());    }     }}

 

继承Identity的UserManager类:

 

public class AppStoreUserManager : UserManager<AppStoreUser>{  public AppStoreUserManager(IUserStore<AppStoreUser> store) : base(store)  {}}

 

2、OWIN Middleware

 

在NuGet中输入owin,确保已经安装如下组件:

 

Microsoft.Owin.Host.SystemWeb
Microsoft.Owin
Microsoft ASP.NET Web API 2.2 OWIN
Microsoft.Owin.Security
Microsoft.Owin.Security.OAth
Microsoft.Owin.Security.Cookies (optional)
Microsoft ASP.NET Identity Owin
OWIN

 

在项目根下创建Startup.cs部分类。

 

[assembly: OwinStartup(typeof(Store.Startup))]namespace Store{  public partial class Startup  {    public void Configuration(IAppBuilder app)    {      ConfigureStoreAuthentication(app);    }  }}

 

在App_Start中创建Startup.cs部分类。

 

//启用OWIN的Bearer Token Authenticationpublic partial class Startup{  public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }  public static string PublicClientId { get; private set; }  public void ConfigureStoreAuthentication(IAppBuilder app)  {    // User a single instance of StoreContext and AppStoreUserManager per request    app.CreatePerOwinContext(StoreContext.Create);    app.CreatePerOwinContext<AppStoreUserManager>(AppStoreUserManager.Create);    // Configure the application for OAuth based flow    PublicClientId = "self";    OAuthOptions = new OAuthAuthorizationServerOptions    {      TokenEndpointPath = new PathString("/Token"),      Provider = new ApplicationOAuthProvider(PublicClientId),      AccessTokenExpireTimeSpan = TimeSpan.FromDays(10),      AllowInsecureHttp = true    };    app.UseOAuthBearerTokens(OAuthOptions);  }}

 

在Identity用户管理类中添加如下代码:

 

public class AppStoreUserManager : UserManager<AppStoreUser>{  public AppStoreUserManager(IUserStore<AppStoreUser> store)    : base(store)  {  }  public static AppStoreUserManager Create(IdentityFactoryOptions<AppStoreUserManager> options, IOwinContext context)  {    var manager = new AppStoreUserManager(new UserStore<AppStoreUser>(context.Get<StoreContext>()));    // Configure validation logic for usernames    manager.UserValidator = new UserValidator<AppStoreUser>(manager)    {      AllowOnlyAlphanumericUserNames = false,      RequireUniqueEmail = true    };    // Password Validations    manager.PasswordValidator = new PasswordValidator    {      RequiredLength = 6,      RequireNonLetterOrDigit = false,      RequireDigit = false,      RequireLowercase = true,      RequireUppercase = true,    };    var dataProtectionProvider = options.DataProtectionProvider;    if (dataProtectionProvider != null)    {      manager.UserTokenProvider = new DataProtectorTokenProvider<AppStoreUser>(dataProtectionProvider.Create("ASP.NET Identity"));    }    return manager;  }  public async Task<ClaimsIdentity> GenerateUserIdentityAsync(AppStoreUser user, string authenticationType)  {    var userIdentity = await CreateIdentityAsync(user, authenticationType);    return userIdentity;  }}

 

当在API中需要获取用户的时候,就会调用以上的代码,比如:

 

Request.GetOwinContext().GetUserManager<AppStoreUserManager>();

 

为了能够使用OWIN的功能,还需要实现一个OAuthAuthorizationServerProvider。

 

public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider{  private readonly string _publicClientId;  public ApplicationOAuthProvider(string publicClientId)  {    if (publicClientId == null)    {      throw new ArgumentNullException("publicClientId");    }    _publicClientId = publicClientId;  }  public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)  {    var userManager = context.OwinContext.GetUserManager<AppStoreUserManager>();    AppStoreUser user = await userManager.FindAsync(context.UserName, context.Password);    if (user == null)    {      context.SetError("invalid_grant", "Invalid username or password.");      return;    }    ClaimsIdentity oAuthIdentity = await userManager.GenerateUserIdentityAsync(user, OAuthDefaults.AuthenticationType);    AuthenticationProperties properties = new AuthenticationProperties();     AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties);    context.Validated(ticket);  }  public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)  {    if (context.ClientId == null)    {      context.Validated();    }    return Task.FromResult<object>(null);  }}

 


OWIN这个中间件的工作原理大致是:

 

→对Token的请求过来
→OWIN调用以上的GrantResourceOwnerCredentials方法
→OAuthAuthorizationServerProvider获取UerManager的实例
→OAuthAuthorizationServerProvider创建access token
→OAuthAuthorizationServerProvider创建access token给响应
→Identity的UserManager检查用户的credentials是否有效
→Identity的UserManager创建ClaimsIdentity

 

接着,在WebApiConfig中配置,让API只接受bearer token authentication。

 

public static class WebApiConfig{  public static void Register(HttpConfiguration config)  {    // Web API configuration and services    // Configure Web API to use only bearer token authentication.    config.SuppressDefaultHostAuthentication();    config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));    // Web API routes    config.MapHttpAttributeRoutes();  }}

 

在需要验证的控制器上加上Authorize特性。

 

[Authorize]public class OrdersController : ApiController{}

 

AccountController用来处理用户的相关事宜。

 

[Authorize][RoutePrefix("api/Account")]public class AccountController : ApiController{  //private const string LocalLoginProvider = "Local";  private AppStoreUserManager _userManager;  public AccountController()  {  }  public AccountController(AppStoreUserManager userManager,    ISecureDataFormat<AuthenticationTicket> accessTokenFormat)  {    UserManager = userManager;    AccessTokenFormat = accessTokenFormat;  }  public AppStoreUserManager UserManager  {    get    {      return _userManager ?? Request.GetOwinContext().GetUserManager<AppStoreUserManager>();    }    private set    {      _userManager = value;    }  }  public ISecureDataFormat<AuthenticationTicket> AccessTokenFormat { get; private set; }  // POST api/Account/Register  [AllowAnonymous]  [Route("Register")]  public async Task<IHttpActionResult> Register(RegistrationModel model)  {    if (!ModelState.IsValid)    {      return BadRequest(ModelState);    }    var user = new AppStoreUser() { UserName = model.Email, Email = model.Email };    IdentityResult result = await UserManager.CreateAsync(user, model.Password);    if (!result.Succeeded)    {      return GetErrorResult(result);    }    return Ok();  }  protected override void Dispose(bool disposing)  {    if (disposing && _userManager != null)    {      _userManager.Dispose();      _userManager = null;    }    base.Dispose(disposing);  }  #region Helpers  private IAuthenticationManager Authentication  {    get { return Request.GetOwinContext().Authentication; }  }  private IHttpActionResult GetErrorResult(IdentityResult result)  {    if (result == null)    {      return InternalServerError();    }    if (!result.Succeeded)    {      if (result.Errors != null)      {        foreach (string error in result.Errors)        {          ModelState.AddModelError("", error);        }      }      if (ModelState.IsValid)      {        // No ModelState errors are available to send, so just return an empty BadRequest.        return BadRequest();      }      return BadRequest(ModelState);    }    return null;  }  #endregion}

 

3、AngularJS

 

在前端,把token相关的常量放到主module中去。

 

angular.module('gadgetsStore')  .constant('gadgetsUrl', 'http://localhost:61691/api/gadgets')  .constant('ordersUrl', 'http://localhost:61691/api/orders')  .constant('categoriesUrl', 'http://localhost:61691/api/categories')  .constant('tempOrdersUrl', 'http://localhost:61691/api/sessions/temporders')  .constant('registerUrl', '/api/Account/Register')  .constant('tokenUrl', '/Token')  .constant('tokenKey', 'accessToken')  .controller('gadgetStoreCtrl', function ($scope, $http, $location, gadgetsUrl, categoriesUrl, ordersUrl, tempOrdersUrl, cart, tokenKey) {

 

提交订单的时候需要把token写到headers的Authorization属性中去。

 

$scope.sendOrder = function (shippingDetails) {  var token = sessionStorage.getItem(tokenKey);  console.log(token);  var headers = {};  if (token) {    headers.Authorization = 'Bearer ' + token;  }  var order = angular.copy(shippingDetails);  order.gadgets = cart.getProducts();  $http.post(ordersUrl, order, { headers: { 'Authorization': 'Bearer ' + token } })  .success(function (data, status, headers, config) {    $scope.data.OrderLocation = headers('Location');    $scope.data.OrderID = data.OrderID;    cart.getProducts().length = 0;    $scope.saveOrder();    $location.path("/complete");  })  .error(function (data, status, headers, config) {    if (status != 401)      $scope.data.orderError = data.Message;    else {      $location.path("/login");    }  }).finally(function () {  });}

 

在主module中增加登出和注册用户的功能。

 

$scope.logout = function () {  sessionStorage.removeItem(tokenKey);}$scope.createAccount = function () {  $location.path("/register");}

 

当然还需要添加对应的路由:

 

 $routeProvider.when("/login", {    templateUrl: "app/views/login.html"  });$routeProvider.when("/register", {    templateUrl: "app/views/register.html"  });

 

再往主module中添加一个controller,用来处理用户账户相关事宜。

 

angular.module("gadgetsStore")  .controller('accountController', function ($scope, $http, $location, registerUrl, tokenUrl, tokenKey) {  $scope.hasLoginError = false;  $scope.hasRegistrationError = false;  // Registration  $scope.register = function () {    $scope.hasRegistrationError = false;    $scope.result = '';    var data = {      Email: $scope.registerEmail,      Password: $scope.registerPassword,      ConfirmPassword: $scope.registerPassword2    };    $http.post(registerUrl, JSON.stringify(data))        .success(function (data, status, headers, config) {          $location.path("/login");        }).error(function (data, status, headers, config) {          $scope.hasRegistrationError = true;          var errorMessage = data.Message;          console.log(data);          $scope.registrationErrorDescription = errorMessage;          if (data.ModelState['model.Email'])            $scope.registrationErrorDescription += data.ModelState['model.Email'];          if (data.ModelState['model.Password'])            $scope.registrationErrorDescription += data.ModelState['model.Password'];          if (data.ModelState['model.ConfirmPassword'])            $scope.registrationErrorDescription += data.ModelState['model.ConfirmPassword'];          if (data.ModelState[''])            $scope.registrationErrorDescription += data.ModelState[''];        }).finally(function () {        });  }  $scope.login = function () {    $scope.result = '';    var loginData = {      grant_type: 'password',      username: $scope.loginEmail,      password: $scope.loginPassword    };    $http({      method: 'POST',      url: tokenUrl,      data: $.param(loginData),      headers: {        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'      }    }).then(function (result) {      console.log(result);      $location.path("/submitorder");      sessionStorage.setItem(tokenKey, result.data.access_token);      $scope.hasLoginError = false;      $scope.isAuthenticated = true;    }, function (data, status, headers, config) {      $scope.hasLoginError = true;      $scope.loginErrorDescription = data.data.error_description;    });  }});

 

有关登录页:

 

<div ng-controller="accountController">  <form role="form">     <input name="email" type="email" ng-model="loginEmail" autofocus="">     <input name="password" type="password" ng-model="loginPassword" value="">         <div ng-show="hasLoginError">      <a href="#" ng-bind="loginErrorDescription"></a>     </div>         <a href="" ng-click="login()">Login</a>     <a href="" ng-click="createAccount()">Create account</a>  </form></div>

 

有关注册页:

 

<div ng-controller="accountController">  <form role="form">    <input name="email" type="email" ng-model="registerEmail" autofocus="">    <input name="password" type="password" ng-model="registerPassword" value="">    <input name="confirmPassword" type="password" ng-model="registerPassword2" value="">        <div ng-show="hasRegistrationError">      <a href="#" ng-bind="registrationErrorDescription"></a>    </div>    <a href="" ng-click="register()">Create account</a  </form></div>

 

在购物车摘要区域添加一个登出按钮。

 

<a href="" ng-show="isUserAuthenticated()" ng-click="logout()">Logout</a>

 

最后可以把账户相关封装在一个服务中。

 

angular.module("gadgetsStore")  .service('accountService', function ($http, registerUrl, tokenUrl, tokenKey) {    this.register = function (data) {      var request = $http.post(registerUrl, data);      return request;    }    this.generateAccessToken = function (loginData) {      var requestToken = $http({        method: 'POST',        url: tokenUrl,        data: $.param(loginData),        headers: {          'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'        }      });      return requestToken;    }    this.isUserAuthenticated = function () {      var token = sessionStorage.getItem(tokenKey);      if (token)        return true;      else        return false;    }    this.logout = function () {      sessionStorage.removeItem(tokenKey);    }  });

 

把有关订单相关,封装在storeService.js中:

 

angular.module("gadgetsStore")  .service('storeService', function ($http, gadgetsUrl, categoriesUrl, tempOrdersUrl, ordersUrl, tokenKey) {    this.getGadgets = function () {      var request = $http.get(gadgetsUrl);      return request;    }    this.getCategories = function () {      var request = $http.get(categoriesUrl);      return request;    }    this.submitOrder = function (order) {      var token = sessionStorage.getItem(tokenKey);      console.log(token);      var headers = {};      if (token) {        headers.Authorization = 'Bearer ' + token;      }      var request = $http.post(ordersUrl, order, { headers: { 'Authorization': 'Bearer ' + token } });      return request;    }    this.saveTempOrder = function (currentProducts) {      var request = $http.post(tempOrdersUrl, currentProducts);      return request;    }    this.loadTempOrder = function () {      var request = $http.get(tempOrdersUrl);      return request;    }  });

 

本系列结束☺